Compare commits

...

No commits in common. "v1.7.2" and "master" have entirely different histories.

47 changed files with 60 additions and 15467 deletions

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,65 +0,0 @@
---
kind: pipeline
type: docker
name: default
clone:
depth: 1
steps:
- name: installDependencies
image: node:lts
commands:
- npm ci --registry=https://registry.npm.taobao.org
- name: build
image: node:lts
commands:
- npm run fix-compatibility
- npm run build
- tar -czf legacy.tar.gz -C ./dist .
- npm run build -- --modern
- tar -czf modern.tar.gz -C ./dist .
- name: release
image: plugins/gitea-release
settings:
base_url: https://git.ixarea.com
files:
- modern.tar.gz
- legacy.tar.gz
api_key:
from_secret: gitea_token
checksum:
- sha256
when:
event: [tag]
- name: deploy
image: plugins/s3
settings:
bucket: unlock-music
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
source: dist/**/*
strip_prefix: dist/
target: /public
path_style: true
endpoint: https://fs.sz2.ixarea.com
- name: upload
image: plugins/s3
settings:
bucket: unlock-music
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
source: ./*.tar.gz
target: /build/${DRONE_BUILD_NUMBER}
path_style: true
endpoint: https://fs.sz2.ixarea.com

View File

@ -1,39 +0,0 @@
---
name: Bug报告
about: 报告Bug以帮助改进程序
title: ''
labels: bug
assignees: ''
---
* 请按照此模板填写,否则可能立即被关闭
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
**Bug描述**
简要地复述你遇到的Bug
**复现方法**
描述复现方法,必要时请提供样本文件
**程序截图或者Console报错信息**
如果可以请提供二者之一
**环境信息:**
- 操作系统和浏览器:
- 程序版本:
- 获取音乐文件所使用的客户端及其版本信息:
**附加信息**
其他能够帮助确认问题的信息

View File

@ -1,26 +0,0 @@
---
name: 新功能
about: 对于程序新的想法或建议
title: ''
labels: enhancement
assignees: ''
---
- 请按照此模板填写,否则可能立即被关闭
**背景和说明**
简要说明产生此想法的背景和此想法的具体内容
**实现途径**
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
**附加信息**
更多你想要表达的内容

View File

@ -1,66 +0,0 @@
name: Build
on:
push:
paths:
- "**/*.js"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
pull_request:
branches: [ master ]
types: [ opened, synchronize, reopened ]
paths:
- "**/*.js"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
build: [ legacy, modern ]
include:
- build: legacy
BUILD_ARGS:
- build: modern
BUILD_ARGS: "-- --modern"
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Get npm cache directory
id: npm-cache
run: echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v2
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- name: Build
env:
GZIP: "--best"
run: |
npm run build ${{ matrix.BUILD_ARGS }}
tar -czvf dist.tar.gz -C ./dist .
- name: Publish artifact
uses: actions/upload-artifact@v2
with:
name: unlock-music-${{ matrix.build }}.tar.gz
path: ./dist.tar.gz

View File

@ -1,121 +0,0 @@
name: Release and GitHub Pages
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Get npm cache directory
id: npm-cache
run: echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v2
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- name: Build Legacy
env:
GZIP: "--best"
run: |
npm run build
tar -czf legacy.tar.gz -C ./dist .
zip -rJ9 legacy.zip ./dist
- name: Build Modern
env:
GZIP: "--best"
run: |
npm run build -- --modern
tar -czf modern.tar.gz -C ./dist .
zip -rJ9 modern.zip ./dist
- name: Checksum
run: sha256sum *.tar.gz *.zip > sha256sum.txt
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
- name: Get current time
id: date
run: echo "::set-output name=date::$(date +'%Y/%m/%d')"
- name: Create a Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: "Build ${{ steps.date.outputs.date }}"
draft: true
- name: Upload Release Assets - legacy.tar.gz
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.tar.gz
asset_name: legacy.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - legacy.zip
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.zip
asset_name: legacy.zip
asset_content_type: application/zip
- name: Upload Release Assets - modern.tar.gz
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.tar.gz
asset_name: modern.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - modern.zip
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.zip
asset_name: modern.zip
asset_content_type: application/zip
- name: Upload Release Assets - sha256sum.txt
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sha256sum.txt
asset_name: sha256sum.txt
asset_content_type: text/plain

21
.gitignore vendored
View File

@ -1,21 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2020 MengYX
Copyright (c) 2019-2021 MengYX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,46 +1,66 @@
# Unlock Music 音乐解锁
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- unlock-music项目是以学习和技术研究的初衷创建的修改、再分发时请遵循[License](https://github.com/ix64/unlock-music/blob/master/LICENSE)
- Unlock Music的CLI版本正在开发中。
- 我们新建了Telegram群组欢迎加入[https://t.me/unlock_music_chat](https://t.me/unlock_music_chat)
- [其他测试版工具](https://github.com/ix64/unlock-music/wiki/%E5%85%B6%E4%BB%96%E9%9F%B3%E4%B9%90%E6%A0%BC%E5%BC%8F%E5%B7%A5%E5%85%B7)
- [相关的其他项目](https://github.com/ix64/unlock-music/wiki/%E5%92%8CUnlockMusic%E7%9B%B8%E5%85%B3%E7%9A%84%E9%A1%B9%E7%9B%AE)
- ![Release and GitHub Pages](https://github.com/ix64/unlock-music/workflows/Release%20and%20GitHub%20Pages/badge.svg)
**由于DMCA Takedown暂时移除仓库所有代码以及Commits**
# 特性
## 支持的格式
- [x] QQ音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/[.tkm](https://github.com/ix64/unlock-music/issues/9))
- [x] 写入封面图片
- [x] Moo音乐格式 ([.bkcmp3/.bkcflac](https://github.com/ix64/unlock-music/issues/11))
- [x] QQ音乐Tm格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ音乐新格式 (实验性支持)
- [x] .mflac
- [x] [.mgg](https://github.com/ix64/unlock-music/issues/3)
- [x] 网易云音乐格式 (.ncm)
- [x] 补全ncm的ID3/FlacMeta信息
- [x] 虾米音乐格式 (.xm) (测试阶段)
- [x] 酷我音乐格式 (.kwm) (测试阶段)
- [x] 酷狗音乐格式 (.kgm) ([CLI版本](https://github.com/ix64/unlock-music/wiki/%E5%85%B6%E4%BB%96%E9%9F%B3%E4%B9%90%E6%A0%BC%E5%BC%8F%E5%B7%A5%E5%85%B7#%E9%85%B7%E7%8B%97%E9%9F%B3%E4%B9%90-kgmvpr%E8%A7%A3%E9%94%81%E5%B7%A5%E5%85%B7))
- 项目新域名:[unlock-music.dev](https://unlock-music.dev)
- 获取更多信息,欢迎加入 Telegram 群组 [`@unlock_music_chat`][tg_group]
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循 [License][license]
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli][repo_cli] 找到,大批量转换建议使用 CLI 版本。
- [相关的其他项目][related_projects]
## 其他特性
- [x] 在浏览器中解锁
- [x] 拖放文件
- [x] 在线播放
- [x] 批量解锁
- [x] 渐进式Web应用
- [x] 多线程
![Test Build](https://github.com/unlock-music/unlock-music/workflows/Test%20Build/badge.svg)
![GitHub releases](https://img.shields.io/github/downloads/unlock-music/unlock-music/total)
![Docker Pulls](https://img.shields.io/docker/pulls/ix64/unlock-music)
[license]: https://github.com/unlock-music/unlock-music/blob/master/LICENSE
# 使用方法
## 使用已构建版本
- 从[GitHub Release](https://github.com/ix64/unlock-music/releases/latest)下载已构建的版本
- 本地使用请下载`legacy版本``modern版本`只能通过**http/https协议**访问)
[repo_cli]: https://github.com/unlock-music/cli
[tg_group]: https://t.me/unlock_music_chat
[related_projects]: https://github.com/unlock-music/unlock-music/wiki/和UnlockMusic相关的项目
## 使用方法
### 安装浏览器扩展
[![Chrome Web Store](https://storage.googleapis.com/chrome-gcs-uploader.appspot.com/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/gldlhhhmienbhlpkfanjpmffdjblmegd)
[<img src="https://developer.microsoft.com/en-us/store/badges/images/Chinese_Simplified_get-it-from-MS.png" height="60" alt="Microsoft Edge Addons"/>](https://microsoftedge.microsoft.com/addons/detail/ggafoipegcmodfhakdkalpdpcdkiljmd)
[![Firefox Browser Addons](https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/zh-CN/firefox/addon/unlock-music/)
### 使用已构建版本
- 从[GitHub Release](https://github.com/unlock-music/unlock-music/releases/latest)下载已构建的版本
- 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**
## 自行构建
### 使用 Docker 镜像
```shell
docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
```
### 自行构建
- 环境要求
- nodejs
- nodejs (v16.x)
- npm
1. 获取项目源代码后执行 `npm install` 安装相关依赖
2. 执行 `npm run build` 即可进行构建,构建输出为 dist 目录
- `npm run serve` 可用于开发
1. 获取项目源代码后安装相关依赖:
```sh
npm ci
```
2. 然后进行构建。编译后的文件保存到 dist 目录下:
```sh
npm run build
```
- 如果是用于开发,可以执行 `npm run serve`
3. 如需构建浏览器扩展build 完成后还需要执行:
```sh
npm run make-extension
```

View File

@ -1,11 +0,0 @@
module.exports = {
presets: [
'@vue/app'
],
plugins: [
["component", {
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}]
]
};

13051
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
{
"name": "unlock-music",
"version": "1.7.2",
"updateInfo": "修复.qmc解锁的一些问题",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {
"type": "git",
"url": "https://github.com/ix64/unlock-music"
},
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"fix-compatibility": "node ./src/fix-compatibility.js"
},
"dependencies": {
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.8.1",
"crypto-js": "^4.0.0",
"element-ui": "^2.14.1",
"iconv-lite": "^0.5.1",
"jimp": "^0.14.0",
"metaflac-js": "^1.0.5",
"music-metadata-browser": "^2.1.7",
"node-sass": "^4.14.1",
"register-service-worker": "^1.7.2",
"sass-loader": "^10.0.2",
"vue": "^2.6.12"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.9",
"@vue/cli-plugin-pwa": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"babel-plugin-component": "^1.1.1",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.12",
"workerize-loader": "^1.3.0"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,17 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" t="1566718842150" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1244" width="16" height="16">
<defs>
<style type="text/css"></style>
</defs>
<path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#2674FD" p-id="1245"></path>
<path d="M512 512m-425.57245 0a425.57245 425.57245 0 1 0 851.1449 0 425.57245 425.57245 0 1 0-851.1449 0Z"
fill="#FFFFFF" p-id="1246"></path>
<path d="M512 512m-214.271074 0a214.271074 214.271074 0 1 0 428.542148 0 214.271074 214.271074 0 1 0-428.542148 0Z"
fill="#FFE41F" p-id="1247"></path>
<path d="M635.968268 408.15377l-89.224127-8.722657a15.980441 15.980441 0 0 0-16.859365 11.479283l-1.784482 6.28564a22.372617 22.372617 0 0 0-2.237262 5.193643l-26.088069 91.754363a67.410825 67.410825 0 1 0 12.984108 61.498063c0.332926-1.185216 0.639218-2.370432 0.905558-3.555648h0.093219l33.106147-116.457461 48.527271-1.891019a48.84688 48.84688 0 0 0 37.767108-20.308477l8.735974-12.158452a8.336463 8.336463 0 0 0-5.92608-13.117278z"
fill="#FFFFFF" p-id="1248"></path>
<path d="M214.231123 503.383879c4.527792-160.563477 136.113403-289.339194 297.768877-289.339194s293.241085 128.775717 297.768877 289.339194h214.151221C1019.339038 224.61841 791.910734 0 512 0S4.647645 224.61841 0.079902 503.383879z"
fill="#2674FD" p-id="1249"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,72 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta content="webkit" name="renderer">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<!--@formatter:off-->
<script>var _paq=window._paq||[];_paq.push(["setRequestMethod","POST"],["trackPageView"],["enableLinkTracking"],["setSiteId","2"],["setTrackerUrl","https://stats.ixarea.com/ixarea-stats/report"]);</script>
<!--@formatter:on-->
<script async src="https://stats.ixarea.com/ixarea-stats.js"></script>
<title>音乐解锁 - By IXarea</title>
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
<!--@formatter:off-->
<style>#loader{position:absolute;left:50%;top:50%;z-index:1010;margin:-75px 0 0 -75px;border:16px solid #f3f3f3;border-radius:50%;border-top:16px solid #1db1ff;width:120px;height:120px;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}#loader-mask{text-align:center;position:absolute;width:100%;height:100%;bottom:0;left:0;right:0;top:0;z-index:1009;background-color:rgba(242,246,252,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(0,0,0,.85)}#loader-mask a{color:#ddd}#loader-mask a:hover{color:#1db1ff}}#loader-source{font-size:1.5rem}#loader-tips-timeout{font-size:1.2rem}</style>
<!--@formatter:on-->
</head>
<body>
<div id="loader-mask">
<div id="loader"></div>
<noscript>
<h3 id="loader-js">请启用JavaScript</h3>
<img alt=""
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
style="border:0"/>
</noscript>
<h3 id="loader-source"> 请勿直接运行源代码! </h3>
<div hidden id="loader-tips-outdated">
<h2>您可能在使用不受支持的<span style="color:#f00;">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color:#f00;">“极速模式”</span> 解决此问题。</h3>
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
</div>
<h3 hidden id="loader-tips-timeout">
音乐解锁采用了一些新特性!建议使用
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
| <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</h3>
</div>
<div id="app"></div>
<script>
(function () {
setTimeout(function () {
var ele = document.getElementById("loader-tips-timeout");
if (ele != null) {
ele.hidden = false;
}
}, 2000);
var ua = navigator && navigator.userAgent;
var detected = (function () {
var m;
if (!ua) return true;
if (/MSIE |Trident\//.exec(ua)) return true; // no IE
m = /Edge\/([\d.]+)/.exec(ua); // Edge >= 17
if (m && Number(m[1]) < 17) return true;
m = /Chrome\/([\d.]+)/.exec(ua); // Chrome >= 58
if (m && Number(m[1]) < 58) return true;
m = /Firefox\/([\d.]+)/.exec(ua); // Firefox >= 45
return m && Number(m[1]) < 45;
})();
if (detected) {
document.getElementById('loader-tips-outdated').hidden = false;
document.getElementById("loader-tips-timeout").hidden = false;
}
})();
</script>
</body>
</html>

View File

@ -1,21 +0,0 @@
{
"name": "音乐解锁 - By IXarea",
"short_name": "音乐解锁",
"description": "在任何设备上解锁已购的加密音乐支持QQ音乐与网易云音乐",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}

Binary file not shown.

View File

@ -1,184 +0,0 @@
<template>
<el-container id="app">
<el-main>
<x-upload v-on:handle_error="showFail" v-on:handle_finish="showSuccess"></x-upload>
<div id="app-control">
<el-row class="mb-3">
<span>歌曲命名格式</span>
<el-radio label="1" name="format" v-model="download_format">歌手-歌曲名</el-radio>
<el-radio label="2" name="format" v-model="download_format">歌曲名</el-radio>
<el-radio label="3" name="format" v-model="download_format">歌曲名-歌手</el-radio>
<el-radio label="4" name="format" v-model="download_format">同原文件名</el-radio>
</el-row>
<el-row>
<el-button @click="handleDownloadAll" icon="el-icon-download" plain>下载全部</el-button>
<el-button @click="handleDeleteAll" icon="el-icon-delete" plain type="danger">清除全部</el-button>
<el-tooltip class="item" effect="dark" placement="top-start">
<div slot="content">
当您使用此工具进行大量文件解锁的时候建议开启此选项<br/>
开启后解锁结果将不会存留于浏览器中防止内存不足
</div>
<el-checkbox border class="ml-2" v-model="instant_download">立即保存</el-checkbox>
</el-tooltip>
</el-row>
</div>
<audio :autoplay="playing_auto" :src="playing_url" controls/>
<x-preview :download_format="download_format" :table-data="tableData"
v-on:music_changed="changePlaying"></x-preview>
</el-main>
<el-footer id="app-footer">
<el-row>
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>(v<span
v-text="version"></span>)移除已购音乐的加密保护
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
目前支持网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>
</el-row>
<el-row>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span>Copyright &copy; 2019-</span><span v-text="(new Date()).getFullYear()"></span> MengYX
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放源代码
</el-row>
</el-footer>
</el-container>
</template>
<script>
import upload from "./component/upload"
import preview from "./component/preview"
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/util"
import config from "../package"
import {IXAREA_API_ENDPOINT} from "./decrypt/util";
export default {
name: 'app',
components: {
xUpload: upload,
xPreview: preview
},
data() {
return {
version: config.version,
activeIndex: '1',
tableData: [],
playing_url: "",
playing_auto: false,
download_format: '1',
instant_download: false,
}
},
created() {
this.$nextTick(function () {
this.finishLoad();
});
},
methods: {
async finishLoad() {
const mask = document.getElementById("loader-mask");
if (!!mask) mask.remove();
let updateInfo;
try {
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({"Version": this.version})
});
updateInfo = await resp.json();
} catch (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'
});
}
},
showSuccess(data) {
if (data.status) {
if (this.instant_download) {
DownloadBlobMusic(data, this.download_format);
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)]);
}
} else {
this.showFail(data.message, data.rawFilename + "." + data.rawExt)
}
},
showFail(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", errInfo, filename]);
}
console.error(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) {
DownloadBlobMusic(this.tableData[index], this.download_format);
index++;
} else {
clearInterval(c);
}
}, 300);
}
},
}
</script>
<style lang="scss">
@import "scss/unlock-music";
</style>

View File

@ -1,71 +0,0 @@
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div class="image-slot el-image__error" slot="error">
暂无封面
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="歌曲">
<template slot-scope="scope">
<span>{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="歌手">
<template slot-scope="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑">
<template slot-scope="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handlePlay(scope.$index, scope.row)"
circle icon="el-icon-video-play" type="success">
</el-button>
<el-button @click="handleDownload(scope.row)"
circle icon="el-icon-download">
</el-button>
<el-button @click="handleDelete(scope.$index, scope.row)"
circle icon="el-icon-delete" type="danger">
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import {DownloadBlobMusic, RemoveBlobMusic} from './util'
export default {
name: "preview",
props: {
tableData: {type: Array, required: true},
download_format: {type: String, required: true}
},
methods: {
handlePlay(index, row) {
this.$emit("music_changed", row.file);
},
handleDelete(index, row) {
RemoveBlobMusic(row);
this.tableData.splice(index, 1);
},
handleDownload(row) {
DownloadBlobMusic(row, this.download_format)
},
}
}
</script>
<style scoped>
</style>

View File

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

View File

@ -1,30 +0,0 @@
export function DownloadBlobMusic(data, format) {
const a = document.createElement('a');
a.href = data.file;
switch (format) {
default:
case "1":
a.download = data.artist + " - " + data.title + "." + data.ext;
break;
case "2":
a.download = data.title + "." + data.ext;
break;
case "3":
a.download = data.title + " - " + data.artist + "." + data.ext;
break;
case "4":
a.download = data.rawFilename + "." + data.ext;
break;
}
document.body.append(a);
a.click();
a.remove();
}
export function RemoveBlobMusic(data) {
URL.revokeObjectURL(data.file);
if (data.picture.startsWith("blob:")) {
URL.revokeObjectURL(data.picture);
}
}

View File

@ -1,68 +0,0 @@
const NcmDecrypt = require("./ncm");
const KwmDecrypt = require("./kwm");
const XmDecrypt = require("./xm");
const QmcDecrypt = require("./qmc");
const RawDecrypt = require("./raw");
const TmDecrypt = require("./tm");
const KgmDecrypt = require("./kgm");
export async function CommonDecrypt(file) {
let raw_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
let raw_filename = file.name.substring(0, file.name.lastIndexOf("."));
let rt_data;
switch (raw_ext) {
case "ncm":// Netease Mp3/Flac
rt_data = await NcmDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break;
case "kwm":// Kuwo Mp3/Flac
rt_data = await KwmDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break
case "xm": // Xiami Wav/M4a/Mp3/Flac
case "wav":// Xiami/Raw Wav
case "mp3":// Xiami/Raw Mp3
case "flac":// Xiami/Raw Flac
case "m4a":// Xiami/Raw M4a
rt_data = await XmDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break;
case "ogg":// Raw Ogg
rt_data = await RawDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break;
case "tm0":// QQ Music IOS Mp3
case "tm3":// QQ Music IOS Mp3
rt_data = await RawDecrypt.Decrypt(file.raw, raw_filename, "mp3");
break;
case "qmc3"://QQ Music Android Mp3
case "qmc2"://QQ Music Android Ogg
case "qmc0"://QQ Music Android Mp3
case "qmcflac"://QQ Music Android Flac
case "qmcogg"://QQ Music Android Ogg
case "tkm"://QQ Music Accompaniment M4a
case "bkcmp3"://Moo Music Mp3
case "bkcflac"://Moo Music Flac
case "mflac"://QQ Music Desktop Flac
case "mgg": //QQ Music Desktop Ogg
case "666c6163"://QQ Music Weiyun Flac
case "6d7033"://QQ Music Weiyun Mp3
case "6f6767"://QQ Music Weiyun Ogg
case "6d3461"://QQ Music Weiyun M4a
case "776176"://QQ Music Weiyun Wav
rt_data = await QmcDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break;
case "tm2":// QQ Music IOS M4a
case "tm6":// QQ Music IOS M4a
rt_data = await TmDecrypt.Decrypt(file.raw, raw_filename);
break;
case "vpr":
case "kgm":
case "kgma":
rt_data = await KgmDecrypt.Decrypt(file.raw, raw_filename, raw_ext);
break
default:
rt_data = {status: false, message: "不支持此文件格式",}
}
if (!rt_data.rawExt) rt_data.rawExt = raw_ext;
if (!rt_data.rawFilename) rt_data.rawFilename = raw_filename;
console.log(rt_data);
return rt_data;
}

View File

@ -1,120 +0,0 @@
import {AudioMimeType, DetectAudioExt, GetArrayBuffer, GetFileInfo, GetMetaCoverURL, IsBytesEqual} from "./util";
const musicMetadata = require("music-metadata-browser");
const VprHeader = [
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31]
const KgmHeader = [
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
const VprMaskDiff = [0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
0x00]
export async function Decrypt(file, raw_filename, raw_ext) {
try {
if (window.location.protocol === "file:") {
return {
status: false,
message: "请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
}
}
} catch {
}
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (raw_ext === "vpr") {
if (!IsBytesEqual(VprHeader, oriData.slice(0, 0x10)))
return {status: false, message: "Not a valid vpr file!"}
} else {
if (!IsBytesEqual(KgmHeader, oriData.slice(0, 0x10)))
return {status: false, message: "Not a valid kgm/kgma file!"}
}
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
let headerLen = bHeaderLen.getUint32(0, true)
let audioData = oriData.slice(headerLen)
let dataLen = audioData.length
if (audioData.byteLength > 1 << 26) {
return {
status: false,
message: "文件过大,请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
}
}
let key1 = new Uint8Array(17)
key1.set(oriData.slice(0x1c, 0x2c), 0)
if (MaskV2 == null) {
if (!await LoadMaskV2()) {
return {status: false, message: "加载Kgm/Vpr Mask数据失败"}
}
}
for (let i = 0; i < dataLen; i++) {
let med8 = key1[i % 17] ^ audioData[i]
med8 ^= (med8 & 0xf) << 4
let msk8 = GetMask(i)
msk8 ^= (msk8 & 0xf) << 4
audioData[i] = med8 ^ msk8
}
if (raw_ext === "vpr") {
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17]
}
const ext = DetectAudioExt(audioData, "mp3");
const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], {type: mime});
const musicMeta = await musicMetadata.parseBlob(musicBlob);
const info = GetFileInfo(musicMeta.common.artist, musicMeta.common.title, raw_filename);
const imgUrl = GetMetaCoverURL(musicMeta);
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
mime: mime
}
}
function GetMask(pos) {
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
}
let MaskV2 = null;
async function LoadMaskV2() {
try {
let resp = await fetch("./static/kgm.mask", {
method: "GET"
})
MaskV2 = new Uint8Array(await resp.arrayBuffer());
return true
} catch (e) {
console.error(e)
return false
}
}
const MaskV2PreDef = [
0xB8, 0xD5, 0x3D, 0xB2, 0xE9, 0xAF, 0x78, 0x8C, 0x83, 0x33, 0x71, 0x51, 0x76, 0xA0, 0xCD, 0x37,
0x2F, 0x3E, 0x35, 0x8D, 0xA9, 0xBE, 0x98, 0xB7, 0xE7, 0x8C, 0x22, 0xCE, 0x5A, 0x61, 0xDF, 0x68,
0x69, 0x89, 0xFE, 0xA5, 0xB6, 0xDE, 0xA9, 0x77, 0xFC, 0xC8, 0xBD, 0xBD, 0xE5, 0x6D, 0x3E, 0x5A,
0x36, 0xEF, 0x69, 0x4E, 0xBE, 0xE1, 0xE9, 0x66, 0x1C, 0xF3, 0xD9, 0x02, 0xB6, 0xF2, 0x12, 0x9B,
0x44, 0xD0, 0x6F, 0xB9, 0x35, 0x89, 0xB6, 0x46, 0x6D, 0x73, 0x82, 0x06, 0x69, 0xC1, 0xED, 0xD7,
0x85, 0xC2, 0x30, 0xDF, 0xA2, 0x62, 0xBE, 0x79, 0x2D, 0x62, 0x62, 0x3D, 0x0D, 0x7E, 0xBE, 0x48,
0x89, 0x23, 0x02, 0xA0, 0xE4, 0xD5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xFD, 0x16, 0x3A, 0x21, 0x3B,
0x16, 0x0F, 0xC3, 0xB2, 0xBB, 0xB3, 0xE2, 0xBA, 0x3A, 0x3D, 0x13, 0xEC, 0xF6, 0x01, 0x45, 0x84,
0xA5, 0x70, 0x0F, 0x93, 0x49, 0x0C, 0x64, 0xCD, 0x31, 0xD5, 0xCC, 0x4C, 0x07, 0x01, 0x9E, 0x00,
0x1A, 0x23, 0x90, 0xBF, 0x88, 0x1E, 0x3B, 0xAB, 0xA6, 0x3E, 0xC4, 0x73, 0x47, 0x10, 0x7E, 0x3B,
0x5E, 0xBC, 0xE3, 0x00, 0x84, 0xFF, 0x09, 0xD4, 0xE0, 0x89, 0x0F, 0x5B, 0x58, 0x70, 0x4F, 0xFB,
0x65, 0xD8, 0x5C, 0x53, 0x1B, 0xD3, 0xC8, 0xC6, 0xBF, 0xEF, 0x98, 0xB0, 0x50, 0x4F, 0x0F, 0xEA,
0xE5, 0x83, 0x58, 0x8C, 0x28, 0x2C, 0x84, 0x67, 0xCD, 0xD0, 0x9E, 0x47, 0xDB, 0x27, 0x50, 0xCA,
0xF4, 0x63, 0x63, 0xE8, 0x97, 0x7F, 0x1B, 0x4B, 0x0C, 0xC2, 0xC1, 0x21, 0x4C, 0xCC, 0x58, 0xF5,
0x94, 0x52, 0xA3, 0xF3, 0xD3, 0xE0, 0x68, 0xF4, 0x00, 0x23, 0xF3, 0x5E, 0x0A, 0x7B, 0x93, 0xDD,
0xAB, 0x12, 0xB2, 0x13, 0xE8, 0x84, 0xD7, 0xA7, 0x9F, 0x0F, 0x32, 0x4C, 0x55, 0x1D, 0x04, 0x36,
0x52, 0xDC, 0x03, 0xF3, 0xF9, 0x4E, 0x42, 0xE9, 0x3D, 0x61, 0xEF, 0x7C, 0xB6, 0xB3, 0x93, 0x50,
]

View File

@ -1,66 +0,0 @@
import {AudioMimeType, DetectAudioExt, GetArrayBuffer, GetFileInfo, GetMetaCoverURL, IsBytesEqual} from "./util";
const musicMetadata = require("music-metadata-browser");
const MagicHeader = [
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
]
const PreDefinedKey = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
export async function Decrypt(file, raw_filename, raw_ext) {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!IsBytesEqual(MagicHeader, oriData.slice(0, 0x10)))
return {status: false, message: "Not a valid kwm file!"}
let fileKey = oriData.slice(0x18, 0x20)
let mask = createMaskFromKey(fileKey)
let audioData = oriData.slice(0x400);
let lenAudioData = audioData.length;
for (let cur = 0; cur < lenAudioData; ++cur)
audioData[cur] ^= mask[cur % 0x20];
const ext = DetectAudioExt(audioData, "mp3");
const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], {type: mime});
const musicMeta = await musicMetadata.parseBlob(musicBlob);
const info = GetFileInfo(musicMeta.common.artist, musicMeta.common.title, raw_filename);
const imgUrl = GetMetaCoverURL(musicMeta);
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
mime: mime
}
}
function createMaskFromKey(keyBytes) {
let keyView = new DataView(keyBytes.buffer)
let keyStr = keyView.getBigUint64(0, true).toString()
let keyStrTrim = trimKey(keyStr)
let key = new Uint8Array(32)
for (let i = 0; i < 32; i++) {
key[i] = PreDefinedKey[i].charCodeAt() ^ keyStrTrim[i].charCodeAt()
}
return key
}
function trimKey(keyRaw) {
let lenRaw = keyRaw.length;
let out = keyRaw;
if (lenRaw > 32) {
out = keyRaw.slice(0, 32)
} else if (lenRaw < 32) {
out = keyRaw.padEnd(32, keyRaw)
}
return out
}

View File

@ -1,180 +0,0 @@
const CryptoJS = require("crypto-js");
const MetaFlac = require('metaflac-js');
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857");
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728");
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
const musicMetadata = require("music-metadata-browser");
import jimp from 'jimp';
import {
AudioMimeType,
DetectAudioExt,
GetArrayBuffer,
GetFileInfo,
GetWebImage,
IsBytesEqual,
WriteMp3Meta
} from "./util"
export async function Decrypt(file, raw_filename, raw_ext) {
const fileBuffer = await GetArrayBuffer(file);
const dataView = new DataView(fileBuffer);
if (!IsBytesEqual(MagicHeader, new Uint8Array(fileBuffer, 0, 8)))
return {status: false, message: "此ncm文件已损坏"};
const keyDataObj = getKeyData(dataView, fileBuffer, 10);
const keyBox = getKeyBox(keyDataObj.data);
const musicMetaObj = getMetaData(dataView, fileBuffer, keyDataObj.offset);
const musicMeta = musicMetaObj.data;
let audioOffset = musicMetaObj.offset + dataView.getUint32(musicMetaObj.offset + 5, true) + 13;
let audioData = new Uint8Array(fileBuffer, audioOffset);
let lenAudioData = audioData.length;
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
if (musicMeta.album === undefined) musicMeta.album = "";
const artists = [];
if (!!musicMeta.artist) musicMeta.artist.forEach(arr => artists.push(arr[0]));
const info = GetFileInfo(artists.join("; "), musicMeta.musicName, raw_filename);
if (artists.length === 0) artists.push(info.artist);
if (musicMeta.format === undefined) musicMeta.format = DetectAudioExt(audioData, "mp3");
console.log(musicMeta)
const imageInfo = await GetWebImage(musicMeta.albumPic);
while (!!imageInfo.buffer && imageInfo.buffer.byteLength >= 16 * 1024 * 1024) {
let img = await jimp.read(imageInfo.buffer)
await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO)
imageInfo.buffer = await img.getBufferAsync("image/jpeg")
}
console.log(imageInfo)
const mime = AudioMimeType[musicMeta.format]
try {
let musicBlob = new Blob([audioData], {type: mime});
const originalMeta = await musicMetadata.parseBlob(musicBlob);
console.log(originalMeta)
let shouldWrite = !originalMeta.common.album && !originalMeta.common.artists && !originalMeta.common.title
if (musicMeta.format === "mp3") {
audioData = await WriteMp3Meta(
audioData, artists, info.title, musicMeta.album, imageInfo.buffer, musicMeta.albumPic, shouldWrite ? null : originalMeta)
} else if (musicMeta.format === "flac") {
const writer = new MetaFlac(Buffer.from(audioData))
if (shouldWrite) {
writer.setTag("TITLE=" + info.title)
writer.setTag("ALBUM=" + musicMeta.album)
writer.removeTag("ARTIST")
artists.forEach(artist => writer.setTag("ARTIST=" + artist))
}
writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer))
audioData = writer.save()
}
} catch (e) {
console.warn("Error while appending cover image to file " + e)
}
const musicData = new Blob([audioData], {type: mime})
return {
status: true,
title: info.title,
artist: info.artist,
ext: musicMeta.format,
album: musicMeta.album,
picture: imageInfo.url,
file: URL.createObjectURL(musicData),
mime: mime
}
}
function getKeyData(dataView, fileBuffer, offset) {
const keyLen = dataView.getUint32(offset, true);
offset += 4;
const cipherText = new Uint8Array(fileBuffer, offset, keyLen).map(
uint8 => uint8 ^ 0x64
);
offset += keyLen;
const plainText = CryptoJS.AES.decrypt(
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)},
CORE_KEY,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.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;
}
return {offset: offset, data: result.slice(17)};
}
function getKeyBox(keyData) {
const box = new Uint8Array(Array(256).keys());
const keyDataLen = keyData.length;
let j = 0;
for (let i = 0; i < 256; i++) {
j = (box[i] + j + keyData[i % keyDataLen]) & 0xff;
[box[i], box[j]] = [box[j], box[i]];
}
return box.map((_, i, arr) => {
i = (i + 1) & 0xff;
const si = arr[i];
const sj = arr[(i + si) & 0xff];
return arr[(si + sj) & 0xff];
});
}
/**
* @typedef {Object} MusicMetaType
* @property {Number} musicId
* @property {String} musicName
* @property {[[String, Number]]} artist
* @property {String} album
* @property {"flac"|"mp3"} format
* @property {String} albumPic
*/
function getMetaData(dataView, fileBuffer, offset) {
const metaDataLen = dataView.getUint32(offset, true);
offset += 4;
if (metaDataLen === 0) return {data: {}, offset: offset};
const cipherText = new Uint8Array(fileBuffer, offset, metaDataLen).map(
data => data ^ 0x63
);
offset += metaDataLen;
const plainText = CryptoJS.AES.decrypt({
ciphertext: CryptoJS.enc.Base64.parse(
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8)
)
},
META_KEY,
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7}
).toString(CryptoJS.enc.Utf8);
const labelIndex = plainText.indexOf(":");
let result = JSON.parse(plainText.slice(labelIndex + 1));
if (plainText.slice(0, labelIndex) === "dj") {
result = result.mainMusic;
}
if (!!result.albumPic && result.albumPic !== "")
result.albumPic = result.albumPic.replace("http://", "https://") + "?param=500y500";
return {data: result, offset: offset};
}

View File

@ -1,162 +0,0 @@
import {
AudioMimeType,
DetectAudioExt,
GetArrayBuffer,
GetFileInfo,
GetMetaCoverURL,
GetWebImage,
IXAREA_API_ENDPOINT,
WriteMp3Meta
} from "./util";
import {QmcMaskCreate58, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask";
import {fromByteArray as Base64Encode, toByteArray as Base64Decode} from 'base64-js'
const MetaFlac = require('metaflac-js');
const ID3Writer = require("browser-id3-writer");
const iconv = require('iconv-lite');
const decode = iconv.decode
const musicMetadata = require("music-metadata-browser");
const HandlerMap = {
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
"qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"qmcogg": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"qmcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"bkcmp3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"bkcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"tkm": {handler: QmcMaskGetDefault, ext: "m4a", detect: false},
"666c6163": {handler: QmcMaskGetDefault, ext: "flac", detect: false},
"6d7033": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"6f6767": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"6d3461": {handler: QmcMaskGetDefault, ext: "m4a", detect: false},
"776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false}
};
export async function Decrypt(file, raw_filename, raw_ext) {
if (!(raw_ext in HandlerMap)) return {status: false, message: "File type is incorrect!"};
const handler = HandlerMap[raw_ext];
const fileData = new Uint8Array(await GetArrayBuffer(file));
let audioData, seed, keyData;
if (handler.detect) {
const keyLen = new DataView(fileData.slice(fileData.length - 4).buffer).getUint32(0, true)
const keyPos = fileData.length - 4 - keyLen;
audioData = fileData.slice(0, keyPos);
seed = handler.handler(audioData);
keyData = fileData.slice(keyPos, keyPos + keyLen);
if (seed === undefined) seed = await queryKeyInfo(keyData, raw_filename, raw_ext);
if (seed === undefined) return {status: false, message: raw_ext + "格式仅提供实验性支持"};
} else {
audioData = fileData;
seed = handler.handler(audioData);
}
let musicDecoded = seed.Decrypt(audioData);
const ext = DetectAudioExt(musicDecoded, handler.ext);
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], {type: mime});
const musicMeta = await musicMetadata.parseBlob(musicBlob);
for (let metaIdx in musicMeta.native) {
if (musicMeta.native[metaIdx].some(item => item.id === "TCON" && item.value === "(12)")) {
console.warn("The metadata is using gbk encoding")
musicMeta.common.artist = decode(musicMeta.common.artist, "gbk");
musicMeta.common.title = decode(musicMeta.common.title, "gbk");
musicMeta.common.album = decode(musicMeta.common.album, "gbk");
}
}
const info = GetFileInfo(musicMeta.common.artist, musicMeta.common.title, raw_filename);
if (handler.detect) reportKeyUsage(keyData, seed.Matrix128,
info.artist, info.title, musicMeta.common.album, raw_filename, raw_ext);
let imgUrl = GetMetaCoverURL(musicMeta);
if (imgUrl === "") {
imgUrl = await queryAlbumCoverImage(info.artist, info.title, musicMeta.common.album);
if (imgUrl !== "") {
const imageInfo = await GetWebImage(imgUrl);
if (imageInfo.url !== "") {
imgUrl = imageInfo.url
try {
if (ext === "mp3") {
musicDecoded = await WriteMp3Meta(musicDecoded,
info.artist.split(" _ "), info.title, "",
imageInfo.buffer, "Cover", musicMeta)
musicBlob = new Blob([musicDecoded], {type: mime});
} else if (ext === 'flac') {
const writer = new MetaFlac(Buffer.from(musicDecoded))
writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer))
musicDecoded = writer.save()
musicBlob = new Blob([musicDecoded], {type: mime});
} else {
console.info("writing metadata for " + ext + " is not being supported for now")
}
} catch (e) {
console.warn("Error while appending cover image to file " + e)
}
}
}
}
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
mime: mime
}
}
function reportKeyUsage(keyData, maskData, artist, title, album, filename, format) {
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
}),
}).then().catch()
}
async function queryKeyInfo(keyData, filename, format) {
try {
const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}),
});
let data = await resp.json();
return QmcMaskCreate58(Base64Decode(data.Matrix44));
} catch (e) {
console.log(e);
}
}
async function queryAlbumCoverImage(artist, title, album) {
const song_query_url = IXAREA_API_ENDPOINT + "/music/qq-cover"
try {
const params = {Artist: artist, Title: title, Album: album};
let _url = song_query_url + "?";
for (let pKey in params) {
_url += pKey + "=" + encodeURIComponent(params[pKey]) + "&"
}
const resp = await fetch(_url)
if (resp.ok) {
let data = await resp.json();
return song_query_url + "/" + data.Type + "/" + data.Id
}
} catch (e) {
console.log(e);
}
return "";
}

View File

@ -1,273 +0,0 @@
import {FLAC_HEADER, IsBytesEqual, OGG_HEADER} from "./util"
const QMOggPublicHeader1 = [
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x1e, 0x01, 0x76, 0x6f, 0x72,
0x62, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x02, 0x44, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xee, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff];
const QMOggPublicHeader2 = [
0x03, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x2c, 0x00, 0x00, 0x00, 0x58, 0x69, 0x70, 0x68, 0x2e,
0x4f, 0x72, 0x67, 0x20, 0x6c, 0x69, 0x62, 0x56, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x20, 0x49, 0x20,
0x32, 0x30, 0x31, 0x35, 0x30, 0x31, 0x30, 0x35, 0x20, 0x28, 0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84,
0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84, 0x29, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x54,
0x49, 0x54, 0x4c, 0x45, 0x3d];
const QMOggPublicConf1 = [
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0,
0, 0, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 6, 3, 3, 3, 3, 6, 6, 6, 6,
3, 3, 3, 3, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9,
0, 0, 0, 0];
const QMOggPublicConf2 = [
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 3, 0, 1, 3, 3, 3,
3, 3, 3, 3, 3];
const QMCDefaultMaskMatrix = [
0xde, 0x51, 0xfa, 0xc3, 0x4a, 0xd6, 0xca, 0x90,
0x7e, 0x67, 0x5e, 0xf7, 0xd5, 0x52, 0x84, 0xd8,
0x47, 0x95, 0xbb, 0xa1, 0xaa, 0xc6, 0x66, 0x23,
0x92, 0x62, 0xf3, 0x74, 0xa1, 0x9f, 0xf4, 0xa0,
0x1d, 0x3f, 0x5b, 0xf0, 0x13, 0x0e, 0x09, 0x3d,
0xf9, 0xbc, 0x00, 0x11];
class QmcMask {
constructor(matrix, superA, superB) {
if (superA === undefined || superB === undefined) {
if (matrix.length === 44) {
this.Matrix44 = matrix
this.generateMask128from44()
} else {
this.Matrix128 = matrix
this.generateMask44from128()
}
this.generateMask58from128()
} else {
this.Matrix58 = matrix;
this.Super58A = superA;
this.Super58B = superB;
this.generateMask128from58();
this.generateMask44from128()
}
}
generateMask128from58() {
if (this.Matrix58.length !== 56) throw "incorrect mask58 matrix length";
let matrix128 = [];
for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) {
matrix128 = matrix128.concat(
[this.Super58A],
this.Matrix58.slice(7 * rowIdx, 7 * rowIdx + 7),
[this.Super58B],
this.Matrix58.slice(56 - 7 - 7 * rowIdx, 56 - 7 * rowIdx).reverse()
);
}
this.Matrix128 = matrix128;
}
generateMask58from128() {
if (this.Matrix128.length !== 128) throw "incorrect mask128 length";
const superA = this.Matrix128[0], superB = this.Matrix128[8];
let matrix58 = [];
for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) {
let lenStart = 16 * rowIdx;
let lenRightStart = 120 - lenStart;
if (this.Matrix128[lenStart] !== superA || this.Matrix128[lenStart + 8] !== superB) {
throw "decode mask-128 to mask-58 failed"
}
let rowLeft = this.Matrix128.slice(lenStart + 1, lenStart + 8);
let rowRight = this.Matrix128.slice(lenRightStart + 1, lenRightStart + 8).reverse();
if (IsBytesEqual(rowLeft, rowRight)) {
matrix58 = matrix58.concat(rowLeft);
} else {
throw "decode mask-128 to mask-58 failed"
}
}
this.Matrix58 = matrix58;
this.Super58A = superA;
this.Super58B = superB;
}
generateMask44from128() {
if (this.Matrix128.length !== 128) throw "incorrect mask128 matrix length";
let mapping = GetConvertMapping()
this.Matrix44 = []
let idxI44 = 0
mapping.forEach(it256 => {
let it256Len = it256.length
for (let i = 1; i < it256Len; i++) {
if (this.Matrix128[it256[0]] !== this.Matrix128[it256[i]]) {
throw "decode mask-128 to mask-44 failed"
}
}
this.Matrix44[idxI44] = this.Matrix128[it256[0]]
idxI44++
})
}
generateMask128from44() {
if (this.Matrix44.length !== 44) throw "incorrect mask length"
this.Matrix128 = []
let idx44 = 0
GetConvertMapping().forEach(it256 => {
it256.forEach(m => {
this.Matrix128[m] = this.Matrix44[idx44]
})
idx44++
})
}
Decrypt(data) {
let dst = data.slice(0);
let index = -1;
let maskIdx = -1;
for (let cur = 0; cur < data.length; cur++) {
index++;
maskIdx++;
if (index === 0x8000 || (index > 0x8000 && (index + 1) % 0x8000 === 0)) {
index++;
maskIdx++;
}
if (maskIdx >= 128) maskIdx -= 128;
dst[cur] ^= this.Matrix128[maskIdx];
}
return dst;
}
}
export function QmcMaskGetDefault() {
return new QmcMask(QMCDefaultMaskMatrix)
}
export function QmcMaskDetectMflac(data) {
let search_len = Math.min(0x8000, data.length), mask;
for (let block_idx = 0; block_idx < search_len; block_idx += 128) {
try {
mask = new QmcMask(data.slice(block_idx, block_idx + 128));
if (IsBytesEqual(FLAC_HEADER, mask.Decrypt(data.slice(0, FLAC_HEADER.length)))) break;
} catch (e) {
}
}
return mask;
}
export function QmcMaskDetectMgg(data) {
if (data.length < 0x100) return
let matrixConfidence = {};
for (let i = 0; i < 44; i++) matrixConfidence[i] = {};
const page2 = data[0x54] ^ data[0xC] ^ QMOggPublicHeader1[0xC];
const spHeader = QmcGenerateOggHeader(page2)
const spConf = QmcGenerateOggConf(page2)
for (let idx128 = 0; idx128 < spHeader.length; idx128++) {
if (spConf[idx128] === 0) continue;
let idx44 = GetMask44Index(idx128);
let _m = data[idx128] ^ spHeader[idx128]
let confidence = spConf[idx128];
if (_m in matrixConfidence[idx44]) {
matrixConfidence[idx44][_m] += confidence
} else {
matrixConfidence[idx44][_m] = confidence
}
}
let matrix = [];
try {
for (let i = 0; i < 44; i++)
matrix[i] = getMaskConfidenceResult(matrixConfidence[i]);
} catch (e) {
return;
}
const mask = new QmcMask(matrix);
let dx = mask.Decrypt(data.slice(0, OGG_HEADER.length));
if (!IsBytesEqual(OGG_HEADER, dx)) {
return;
}
return mask;
}
export function QmcMaskCreate128(mask128) {
return new QmcMask(mask128)
}
export function QmcMaskCreate58(matrix, superA, superB) {
return new QmcMask(matrix, superA, superB)
}
export function QmcMaskCreate44(mask44) {
return new QmcMask(mask44)
}
/**
* @param confidence {{}}
* @returns {number}
*/
function getMaskConfidenceResult(confidence) {
if (confidence.length === 0) throw "can not match at least one key";
if (confidence.length > 1) console.warn("There are 2 potential value for the mask!")
let result, conf = 0;
for (let idx in confidence) {
if (confidence[idx] > conf) {
result = idx;
conf = confidence[idx];
}
}
return parseInt(result)
}
/**
* @return {number}
*/
const allMapping = [];
const mask128to44 = [];
(function () {
for (let i = 0; i < 128; i++) {
let realIdx = (i * i + 27) % 256
if (realIdx in allMapping) {
allMapping[realIdx].push(i)
} else {
allMapping[realIdx] = [i]
}
}
let idx44 = 0
allMapping.forEach(all128 => {
all128.forEach(_i128 => {
mask128to44[_i128] = idx44
})
idx44++
})
})();
function GetConvertMapping() {
return allMapping;
}
function GetMask44Index(idx128) {
return mask128to44[idx128 % 128]
}
function QmcGenerateOggHeader(page2) {
let spec = [page2, 0xFF]
for (let i = 2; i < page2; i++) spec.push(0xFF)
spec.push(0xFF)
return QMOggPublicHeader1.concat(spec, QMOggPublicHeader2)
}
function QmcGenerateOggConf(page2) {
let specConf = [6, 0]
for (let i = 2; i < page2; i++) specConf.push(4)
specConf.push(0)
return QMOggPublicConf1.concat(specConf, QMOggPublicConf2)
}

View File

@ -1,23 +0,0 @@
const musicMetadata = require("music-metadata-browser");
import {AudioMimeType, DetectAudioExt, GetArrayBuffer, GetMetaCoverURL, GetFileInfo} from "./util";
export async function Decrypt(file, raw_filename, raw_ext, detect = true) {
let ext = raw_ext;
if (detect) {
const buffer = new Uint8Array(await GetArrayBuffer(file));
ext = DetectAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
}
const tag = await musicMetadata.parseBlob(file);
const info = GetFileInfo(tag.common.artist, tag.common.title, raw_filename);
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: tag.common.album,
picture: GetMetaCoverURL(tag),
file: URL.createObjectURL(file),
mime: AudioMimeType[ext]
}
}

View File

@ -1,14 +0,0 @@
import {Decrypt as RawDecrypt} from "./raw";
import {GetArrayBuffer} from "./util";
const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70];
export async function Decrypt(file, raw_filename) {
const fileBuffer = await GetArrayBuffer(file);
const audioData = new Uint8Array(fileBuffer);
for (let cur = 0; cur < 8; ++cur) {
audioData[cur] = TM_HEADER[cur];
}
const musicData = new Blob([audioData], {type: "audio/mp4"});
return await RawDecrypt(musicData, raw_filename, "m4a", false)
}

View File

@ -1,125 +0,0 @@
const ID3Writer = require("browser-id3-writer");
const musicMetadata = require("music-metadata-browser");
export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43];
export const MP3_HEADER = [0x49, 0x44, 0x33];
export const OGG_HEADER = [0x4F, 0x67, 0x67, 0x53];
export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70];
export const WMA_HEADER = [
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C,
]
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]
export const AudioMimeType = {
mp3: "audio/mpeg",
flac: "audio/flac",
m4a: "audio/mp4",
ogg: "audio/ogg",
wma: "audio/x-ms-wma",
wav: "audio/x-wav"
};
export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis"
// Also a new draft API: blob.arrayBuffer()
export async function GetArrayBuffer(blobObject) {
return await new Promise(resolve => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.readAsArrayBuffer(blobObject);
});
}
export function GetFileInfo(artist, title, filenameNoExt, separator = "-") {
let newArtist = "", newTitle = "";
let filenameArray = filenameNoExt.split(separator);
if (filenameArray.length > 1) {
newArtist = filenameArray[0].trim();
newTitle = filenameArray[1].trim();
} else if (filenameArray.length === 1) {
newTitle = filenameArray[0].trim();
}
if (typeof artist == "string" && artist !== "") newArtist = artist;
if (typeof title == "string" && title !== "") newTitle = title;
return {artist: newArtist, title: newTitle};
}
/**
* @return {string}
*/
export function GetMetaCoverURL(metadata) {
let pic_url = "";
if (metadata.common.picture !== undefined && metadata.common.picture.length > 0) {
let pic = new Blob([metadata.common.picture[0].data], {type: metadata.common.picture[0].format});
pic_url = URL.createObjectURL(pic);
}
return pic_url;
}
export function IsBytesEqual(first, second) {
// if want wholly check, should length first>=second
return first.every((val, idx) => {
return val === second[idx];
})
}
/**
* @return {string}
*/
export function DetectAudioExt(data, fallbackExt) {
if (IsBytesEqual(MP3_HEADER, data.slice(0, MP3_HEADER.length))) return "mp3";
if (IsBytesEqual(FLAC_HEADER, data.slice(0, FLAC_HEADER.length))) return "flac";
if (IsBytesEqual(OGG_HEADER, data.slice(0, OGG_HEADER.length))) return "ogg";
if (IsBytesEqual(M4A_HEADER, data.slice(4, 4 + M4A_HEADER.length))) return "m4a";
if (IsBytesEqual(WMA_HEADER, data.slice(0, WMA_HEADER.length))) return "wma";
if (IsBytesEqual(WAV_HEADER, data.slice(0, WAV_HEADER.length))) return "wav";
return fallbackExt;
}
export async function GetWebImage(pic_url) {
try {
let resp = await fetch(pic_url);
let mime = resp.headers.get("Content-Type");
if (mime.startsWith("image/")) {
let buf = await resp.arrayBuffer();
let objBlob = new Blob([buf], {type: mime});
let objUrl = URL.createObjectURL(objBlob);
return {"buffer": buf, "src": pic_url, "url": objUrl, "type": mime};
}
} catch (e) {
}
return {"buffer": null, "src": pic_url, "url": "", "type": ""}
}
export async function WriteMp3Meta(audioData, artistList, title, album, pictureData = null, pictureDesc = "Cover", originalMeta = null) {
const writer = new ID3Writer(audioData);
if (originalMeta !== null) {
artistList = originalMeta.common.artists || artistList
title = originalMeta.common.title || title
album = originalMeta.common.album || album
const frames = originalMeta.native['ID3v2.4'] || originalMeta.native['ID3v2.3'] || originalMeta.native['ID3v2.2'] || []
frames.forEach(frame => {
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
try {
writer.setFrame(frame.id, frame.value)
} catch (e) {
}
}
})
}
writer.setFrame('TPE1', artistList)
.setFrame('TIT2', title)
.setFrame('TALB', album);
if (pictureData !== null) {
writer.setFrame('APIC', {
type: 3,
data: pictureData,
description: pictureDesc,
})
}
writer.addTag();
return writer.arrayBuffer;
}

View File

@ -1,67 +0,0 @@
import {AudioMimeType, GetArrayBuffer, GetFileInfo, GetMetaCoverURL, IsBytesEqual} from "./util";
import {Decrypt as RawDecrypt} from "./raw";
const musicMetadata = require("music-metadata-browser");
const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
const FileTypeMap = {
" WAV": ".wav",
"FLAC": ".flac",
" MP3": ".mp3",
" A4M": ".m4a",
}
export async function Decrypt(file, raw_filename, raw_ext) {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!IsBytesEqual(MagicHeader, oriData.slice(0, 4)) ||
!IsBytesEqual(MagicHeader2, oriData.slice(8, 12))) {
if (raw_ext === "xm") {
return {status: false, message: "此xm文件已损坏"}
} else {
return await RawDecrypt(file, raw_filename, raw_ext, true)
}
}
let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
if (!FileTypeMap.hasOwnProperty(typeText)) {
return {status: false, message: "未知的xm文件类型"}
}
let key = oriData[0xf]
let dataOffset = oriData[0xc] | oriData[0xd] << 8 | oriData[0xe] << 16
let audioData = oriData.slice(0x10);
let lenAudioData = audioData.length;
for (let cur = dataOffset; cur < lenAudioData; ++cur)
audioData[cur] = (audioData[cur] - key) ^ 0xff;
const ext = FileTypeMap[typeText];
const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], {type: mime});
const musicMeta = await musicMetadata.parseBlob(musicBlob);
if (ext === "wav") {
//todo:未知的编码方式
console.log(musicMeta.common)
musicMeta.common.album = "";
musicMeta.common.artist = "";
musicMeta.common.title = "";
}
let _sep = raw_filename.indexOf("_") === -1 ? "-" : "_"
const info = GetFileInfo(musicMeta.common.artist, musicMeta.common.title, raw_filename, _sep);
const imgUrl = GetMetaCoverURL(musicMeta);
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
mime: mime,
rawExt: "xm"
}
}

View File

@ -1,25 +0,0 @@
//TODO: Use other method to fix this
// !! Only Temporary Solution
// it seems like that @babel/plugin-proposal-object-rest-spread not working
// to fix up the compatibility for Edge 18 and some older Chromium
// now manually edit the dependency files
const fs = require('fs');
const filePath = "./node_modules/file-type/core.js";
const regReplace = /{\s*([a-zA-Z0-9:,\s]*),\s*\.\.\.([a-zA-Z0-9]*)\s*};/m;
if (fs.existsSync(filePath)) {
console.log("File Found!");
let data = fs.readFileSync(filePath).toString();
const regResult = regReplace.exec(data);
if (regResult != null) {
data = data.replace(regResult[0],
"Object.assign({ " + regResult[1] + " }, " + regResult[2] + ");"
);
fs.writeFileSync(filePath, data);
console.log("Object rest spread in file-type fixed!");
} else {
console.log("No fix needed.");
}
} else {
console.log("File Not Found!");
}

View File

@ -1,47 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import {
Button,
Checkbox,
Col,
Container,
Footer,
Icon,
Image,
Link,
Main,
Notification,
Progress,
Radio,
Row,
Table,
TableColumn,
Tooltip,
Upload
} from 'element-ui';
import 'element-ui/lib/theme-chalk/base.css';
Vue.use(Link);
Vue.use(Image);
Vue.use(Button);
Vue.use(Table);
Vue.use(TableColumn);
Vue.use(Main);
Vue.use(Footer);
Vue.use(Container);
Vue.use(Icon);
Vue.use(Row);
Vue.use(Col);
Vue.use(Upload);
Vue.use(Checkbox);
Vue.use(Radio);
Vue.use(Tooltip);
Vue.use(Progress);
Vue.prototype.$notify = Notification;
Vue.config.productionTip = false;
document.getElementById("loader-source").remove()
new Vue({
render: h => h(App),
}).$mount('#app');

View File

@ -1,30 +0,0 @@
/* eslint-disable no-console */
import {register} from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log('App is being served from cache by a service worker.')
},
registered() {
console.log('Service worker has been registered.')
},
cached() {
console.log('Content has been cached for offline use.')
},
updatefound() {
console.log('New content is downloading.')
},
updated() {
console.log('New content is available.');
window.location.reload();
},
offline() {
console.log('No internet connection found. App is running in offline mode.')
},
error(error) {
console.error('Error during service worker registration:', error)
}
})
}

View File

@ -1,166 +0,0 @@
/*
* name: 样式 - 夜间模式
* author: @KyleBing
* date: 2020-11-24
*/
@media (prefers-color-scheme: dark) {
#app{
color: $dark-text-info;
}
body{
background-color: $dark-bg;
}
// FORM
.el-radio{
&__label{
color: $dark-text-main;
}
&__input{
color: $dark-text-info;
.el-radio__inner{
border-color: $dark-border;
background-color: $dark-btn-bg;
}
}
&.is-checked{
.el-radio__inner{
background-color: $blue;
}
.el-radio__label{
font-weight: bold;
}
}
}
.el-checkbox.is-bordered{
border-color: $dark-border;
.el-checkbox__inner{
background-color: $dark-btn-bg;
border-color: $dark-border;
}
&:hover{
border-color: $dark-border-highlight;
.el-checkbox__inner{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight;
}
.el-checkbox__label{
color: $dark-text-info;
}
}
&.is-checked{
background-color: $blue;
.el-checkbox__inner{
border-color: $dark-btn-bg-highlight;
}
.el-checkbox__label{
color: white;
font-weight: bold;
}
}
}
// BUTTON
.el-button{
background-color: $dark-btn-bg;
border-color: $dark-border;
color: $dark-text-main;
&:active{
transform: translateY(2px);
}
&--default{
&.is-plain {
background-color: $dark-btn-bg;
&:hover {
background-color: $blue;
border-color: $blue;
color: white;
}
}
}
&--danger{
&.is-plain{
border-color: $dark-border;
background-color: $dark-btn-bg;
&:hover{
background-color: $red;
border-color: $red;
}
}
}
}
// 文件拖放区
.el-upload__tip{
color: $dark-text-info;
}
.el-upload-dragger{
background-color: $dark-uploader-bg;
border-color: $dark-border;
.el-upload__text{
color: $dark-text-info;
}
&:hover{
background: $dark-uploader-bg-highlight;
border-color: $dark-border-highlight;
}
}
//TABLE
.el-table{
background-color: $dark-bg-td;
&:before{ // 去除表格末尾的横线
content: none;
}
&__header{
th{
border-bottom-color: $dark-border !important;
}
}
th{
background-color: $dark-bg-th;
color: $dark-text-info;
}
td{
border-bottom-color: $dark-border !important;
}
tr{
background-color: $dark-bg-td;
color: $dark-text-main;
&:hover{
td{
background-color: $dark-bg-th !important;
}
}
}
}
// LINKS
a{
text-decoration: none;
color: darken($dark-color-link, 15%);
&:hover{
color: $dark-color-link;
}
}
// ALERT
.el-notification{
background-color: $dark-btn-bg-highlight;
border-color: $dark-border;
&__title{
color: white;
}
&__content{
color: $dark-text-info;
}
}
}

View File

@ -1,18 +0,0 @@
/*
* 间隔工具集
*/
$gap: 5px;
@for $item from 1 through 7 {
.mt-#{$item} { margin-top : $gap * $item !important;}
.mb-#{$item} { margin-bottom : $gap * $item !important;}
.ml-#{$item} { margin-left : $gap * $item !important;}
.mr-#{$item} { margin-right : $gap * $item !important;}
.m-#{$item} { margin : $gap * $item !important;}
.pt-#{$item} { padding-top : $gap * $item !important;}
.pb-#{$item} { padding-bottom : $gap * $item !important;}
.pl-#{$item} { padding-left : $gap * $item !important;}
.pr-#{$item} { padding-right : $gap * $item !important;}
.p-#{$item} { padding : $gap * $item !important;}
}

View File

@ -1,38 +0,0 @@
body{
font-family: $font-family;
font-size: $fz-main;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
text-align: center;
color: $text-main;
padding-top: 30px;
}
#app-footer a {
padding-left: 0.2em;
padding-right: 0.2em;
}
#app-footer {
text-align: center;
font-size: small;
}
#app-control {
padding-top: 1em;
padding-bottom: 1em;
}
audio{
margin-bottom: 15px; // 播放控件与表格间隔
}
a{
color: darken($color-link, 15%);
&:hover{
color: $color-link;
}
}

View File

@ -1,28 +0,0 @@
// COLORS
$blue : #409EFF;
$red : #F56C6C;
$green : #85ce61;
// TEXT
$text-main : #2C3E50;
$color-link: $blue;
$fz-main: 14px;
$font-family: "Helvetica Neue", Helvetica, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
// DARK MODE
$dark-border : lighten(black, 25%);
$dark-border-highlight : lighten(black, 55%);
$dark-bg : lighten(black, 10%);
$dark-text-main : lighten(black, 90%);
$dark-text-info : lighten(black, 60%);
$dark-uploader-bg : lighten(black, 13%);
$dark-uploader-bg-highlight : lighten(black, 18%);
$dark-btn-bg : lighten(black, 20%);
$dark-btn-bg-highlight : lighten(black, 30%);
$dark-bg-th : lighten(black, 18%);
$dark-bg-td : lighten(black, 13%);
$dark-color-link : $green;

View File

@ -1,5 +0,0 @@
@import "variables";
@import "gaps";
@import "normal";
@import "dark-mode"; // dark-mode 放在 normal 后面以获得更高优先级

View File

@ -1,9 +0,0 @@
module.exports = {
publicPath: '',
productionSourceMap: false,
pwa: {
workboxOptions: {
skipWaiting: true
}
}
};