15 Commits

Author SHA1 Message Date
Emmm Monster
d3898161b9 chore: bump version & update deps 2021-08-08 08:08:56 +08:00
MengYX
5a7a9e3add change ixarea api endpoint 2021-08-08 07:56:28 +08:00
MengYX
652bb1fc32 optimize: imports 2021-08-08 07:51:42 +08:00
MengYX
6737e8c11b optimize: .kgm mask loading 2021-08-08 07:47:28 +08:00
sunhao03
71862538b7 fetch mask file fix on production 2021-08-07 22:13:00 +08:00
Emmm Monster
4251b94b1f chore: update deps & fix audit 2021-07-01 01:33:28 +08:00
Emmm Monster
8fdda048f6 fix: avoid using worker in file protocol 2021-07-01 01:29:04 +08:00
Emmm Monster
39c7294996 chore(ci): build after *.ts changes 2021-06-03 13:15:04 +08:00
Emmm Monster
48f879cb58 simplify: decrypt/ncm-cache & decrypt/common 2021-06-03 13:09:14 +08:00
Emmm Monster
f0875ad175 fix: decrypt/qmc-cache
adapt: decrypt/qmc for qmc-cache
2021-06-03 13:09:02 +08:00
qq1010903229
02a146e069 增加对网易云音乐.uc缓存格式和QQ音乐.cache缓存格式的支持 (#161)
* Update common.ts

* Create ncmcache.ts

* Create qmccache.ts
2021-06-03 13:00:35 +08:00
Emmm Monster
2e31853ffb chore: update deps 2021-05-27 19:53:19 +08:00
Emmm Monster
a7aaf246ae fix: .vpr/.kgm fail in worker 2021-05-27 19:38:42 +08:00
Emmm Monster
4bc0a10c09 feature(sniffer): support .dff 2021-05-25 12:27:19 +08:00
Emmm Monster
3645dd7d01 fix: remove test file 2021-05-25 04:36:32 +08:00
15 changed files with 2284 additions and 2102 deletions

View File

@@ -3,6 +3,7 @@ on:
push: push:
paths: paths:
- "**/*.js" - "**/*.js"
- "**/*.ts"
- "**/*.vue" - "**/*.vue"
- "public/**/*" - "public/**/*"
- "package-lock.json" - "package-lock.json"
@@ -12,6 +13,7 @@ on:
types: [ opened, synchronize, reopened ] types: [ opened, synchronize, reopened ]
paths: paths:
- "**/*.js" - "**/*.js"
- "**/*.ts"
- "**/*.vue" - "**/*.vue"
- "public/**/*" - "public/**/*"
- "package-lock.json" - "package-lock.json"

4157
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "unlock-music", "name": "unlock-music",
"version": "v1.9.0-beta", "version": "v1.9.0",
"updateInfo": "新增写入本地文件系统; 优化.kwm解锁; 支持.acc嗅探; 使用Typescript重构", "updateInfo": "新增写入本地文件系统; 优化.kwm解锁; 支持.acc嗅探; 使用Typescript重构",
"license": "MIT", "license": "MIT",
"description": "Unlock encrypted music file in browser.", "description": "Unlock encrypted music file in browser.",
@@ -18,19 +18,19 @@
"dependencies": { "dependencies": {
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0", "browser-id3-writer": "^4.4.0",
"core-js": "^3.12.1", "core-js": "^3.16.0",
"crypto-js": "^4.0.0", "crypto-js": "^4.1.1",
"element-ui": "^2.15.1", "element-ui": "^2.15.5",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jimp": "^0.16.1", "jimp": "^0.16.1",
"metaflac-js": "^1.0.5", "metaflac-js": "^1.0.5",
"music-metadata-browser": "^2.2.6", "music-metadata-browser": "^2.4.3",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"threads": "^1.6.4", "threads": "^1.6.5",
"vue": "^2.6.12" "vue": "^2.6.14"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.0.1", "@types/crypto-js": "^4.0.2",
"@vue/cli-plugin-babel": "^4.5.13", "@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-pwa": "^4.5.13", "@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13", "@vue/cli-plugin-typescript": "^4.5.13",
@@ -40,8 +40,8 @@
"sass-loader": "^10.2.0", "sass-loader": "^10.2.0",
"semver": "^7.3.5", "semver": "^7.3.5",
"threads-plugin": "^1.4.0", "threads-plugin": "^1.4.0",
"typescript": "~4.1.5", "typescript": "~4.1.6",
"vue-cli-plugin-element": "^1.0.1", "vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.12" "vue-template-compiler": "^2.6.14"
} }
} }

View File

@@ -63,7 +63,7 @@ export default {
} }
}, },
mounted() { mounted() {
if (window.Worker && process.env.NODE_ENV === 'production') { if (window.Worker && window.location.protocol !== "file:" && process.env.NODE_ENV === 'production') {
console.log("Using Worker Pool") console.log("Using Worker Pool")
this.queue = Pool( this.queue = Pool(
() => spawn(new Worker('@/utils/worker.ts')), () => spawn(new Worker('@/utils/worker.ts')),

View File

@@ -1,37 +1,42 @@
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm"; import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
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 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";
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> { export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
let raw_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase(); const raw = SplitFilename(file.name)
let raw_filename = file.name.substring(0, file.name.lastIndexOf("."));
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_filename, raw_ext); rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
break;
case "uc":// Netease Cache
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_filename, 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_filename, 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_filename, 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_filename, "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
@@ -48,23 +53,26 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
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_filename, 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_filename); rt_data = await TmDecrypt(file.raw, raw.name);
break;
case "cache"://QQ Music Cache
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_filename, 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_filename; if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
console.log(rt_data); console.log(rt_data);
return rt_data; return rt_data;
} }

View File

@@ -8,6 +8,7 @@ import {
} from "@/decrypt/utils.ts"; } from "@/decrypt/utils.ts";
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"
const VprHeader = [ const VprHeader = [
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43, 0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
@@ -22,9 +23,6 @@ const VprMaskDiff = [
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> {
if (window?.location?.protocol === "file:") {
throw Error("请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
}
const oriData = new Uint8Array(await GetArrayBuffer(file)); const oriData = new Uint8Array(await GetArrayBuffer(file));
if (raw_ext === "vpr") { if (raw_ext === "vpr") {
@@ -84,8 +82,16 @@ function GetMask(pos: number) {
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`
if (["http:", "https:"].some(v => v == self.location.protocol)) {
if (!!self.document) {// using Web Worker
mask_url = "./static/kgm.mask"
} else {// using Main thread
mask_url = "../static/kgm.mask"
}
}
try { try {
const resp = await fetch("./static/kgm.mask", {method: "GET"}) const resp = await fetch(mask_url, {method: "GET"})
MaskV2 = new Uint8Array(await resp.arrayBuffer()); MaskV2 = new Uint8Array(await resp.arrayBuffer());
return true return true
} catch (e) { } catch (e) {

View File

@@ -11,11 +11,18 @@ import {
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 CryptoJS from "crypto-js"; import AES from "crypto-js/aes";
import PKCS7 from "crypto-js/pad-pkcs7";
import ModeECB from "crypto-js/mode-ecb";
import WordArray from "crypto-js/lib-typedarrays";
import Base64 from "crypto-js/enc-base64";
import EncUTF8 from "crypto-js/enc-utf8";
import EncHex from "crypto-js/enc-hex";
import {DecryptResult} from "@/decrypt/entity"; import {DecryptResult} from "@/decrypt/entity";
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857"); const CORE_KEY = EncHex.parse("687a4852416d736f356b496e62617857");
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728"); const META_KEY = EncHex.parse("2331346C6A6B5F215C5D2630553C2728");
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D]; const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
@@ -67,11 +74,11 @@ class NcmDecrypt {
.map(uint8 => uint8 ^ 0x64); .map(uint8 => uint8 ^ 0x64);
this.offset += keyLen; this.offset += keyLen;
const plainText = CryptoJS.AES.decrypt( const plainText = AES.decrypt(
// @ts-ignore // @ts-ignore
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)}, {ciphertext: WordArray.create(cipherText)},
CORE_KEY, CORE_KEY,
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7} {mode: ModeECB, padding: PKCS7}
); );
const result = new Uint8Array(plainText.sigBytes); const result = new Uint8Array(plainText.sigBytes);
@@ -115,17 +122,18 @@ class NcmDecrypt {
.map(data => data ^ 0x63); .map(data => data ^ 0x63);
this.offset += metaDataLen; this.offset += metaDataLen;
const plainText = CryptoJS.AES.decrypt( WordArray.create()
//@ts-ignore const plainText = AES.decrypt(
// @ts-ignore
{ {
ciphertext: CryptoJS.enc.Base64.parse( ciphertext: Base64.parse(
//@ts-ignore // @ts-ignore
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8) WordArray.create(cipherText.slice(22)).toString(EncUTF8)
) )
}, },
META_KEY, META_KEY,
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7} {mode: ModeECB, padding: PKCS7}
).toString(CryptoJS.enc.Utf8); ).toString(EncUTF8);
const labelIndex = plainText.indexOf(":"); const labelIndex = plainText.indexOf(":");
let result: NcmMusicMeta; let result: NcmMusicMeta;

29
src/decrypt/ncmcache.ts Normal file
View File

@@ -0,0 +1,29 @@
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
import {DecryptResult} from "@/decrypt/entity";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
: Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
let length = buffer.length
for (let i = 0; i < length; i++) {
buffer[i] ^= 163
}
const ext = SniffAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
const tag = await metaParseBlob(file);
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
blob: file,
mime: AudioMimeType[ext]
}
}

View File

@@ -22,9 +22,11 @@ interface Handler {
handler(data?: Uint8Array): QmcMask | undefined handler(data?: Uint8Array): QmcMask | undefined
} }
const HandlerMap: { [key: string]: Handler } = { export const HandlerMap: { [key: string]: Handler } = {
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true}, "mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true}, "mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
"mgg.cache": {handler: QmcMaskDetectMgg, ext: "ogg", detect: false},
"mflac.cache": {handler: QmcMaskDetectMflac, ext: "flac", detect: false},
"qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, "qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
"qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, "qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false},
"qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, "qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false},
@@ -40,8 +42,8 @@ const HandlerMap: { [key: string]: Handler } = {
"776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false} "776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false}
}; };
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> { export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
if (!(raw_ext in HandlerMap)) throw "File type is incorrect!"; if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
const handler = HandlerMap[raw_ext]; const handler = HandlerMap[raw_ext];
const fileData = new Uint8Array(await GetArrayBuffer(file)); const fileData = new Uint8Array(await GetArrayBuffer(file));
@@ -57,6 +59,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
} else { } else {
audioData = fileData; audioData = fileData;
seed = handler.handler(audioData) as QmcMask; seed = handler.handler(audioData) as QmcMask;
if (!seed) throw raw_ext + "格式仅提供实验性支持";
} }
let musicDecoded = seed.Decrypt(audioData); let musicDecoded = seed.Decrypt(audioData);

51
src/decrypt/qmccache.ts Normal file
View File

@@ -0,0 +1,51 @@
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
SplitFilename
} from "@/decrypt/utils.ts";
import {Decrypt as QmcDecrypt, HandlerMap} from "@/decrypt/qmc";
import {DecryptResult} from "@/decrypt/entity";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
export async function Decrypt(file: Blob, raw_filename: string, _: string)
: Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
let length = buffer.length
for (let i = 0; i < length; i++) {
buffer[i] ^= 0xf4
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
}
let ext = SniffAudioExt(buffer, "");
const newName = SplitFilename(raw_filename)
let audioBlob: Blob
if (ext !== "" || newName.ext === "mp3") {
audioBlob = new Blob([buffer], {type: AudioMimeType[ext]})
} else if (newName.ext in HandlerMap) {
audioBlob = new Blob([buffer], {type: "application/octet-stream"})
return QmcDecrypt(audioBlob, newName.name, newName.ext);
} else {
throw "不支持的QQ音乐缓存格式"
}
const tag = await metaParseBlob(audioBlob);
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(audioBlob),
blob: audioBlob,
mime: AudioMimeType[ext]
}
}

View File

@@ -12,13 +12,16 @@ export const WMA_HEADER = [
] ]
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 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"
}; };
@@ -39,6 +42,7 @@ export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): s
if (BytesHasPrefix(data, WAV_HEADER)) return "wav" if (BytesHasPrefix(data, WAV_HEADER)) return "wav"
if (BytesHasPrefix(data, WMA_HEADER)) return "wma" if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
if (BytesHasPrefix(data, AAC_HEADER)) return "aac" if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
if (BytesHasPrefix(data, DFF_HEADER)) return "dff"
return fallback_ext; return fallback_ext;
} }
@@ -156,3 +160,11 @@ export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: I
} }
return writer.save() return writer.save()
} }
export function SplitFilename(n: string): { name: string; ext: string } {
const pos = n.lastIndexOf(".")
return {
ext: n.substring(pos + 1).toLowerCase(),
name: n.substring(0, pos)
}
}

9
src/shims-fs.d.ts vendored
View File

@@ -6,6 +6,10 @@ interface FileSystemCreateWritableOptions {
keepExistingData?: boolean keepExistingData?: boolean
} }
interface FileSystemRemoveOptions {
recursive?: boolean
}
interface FileSystemFileHandle { interface FileSystemFileHandle {
getFile(): Promise<File>; getFile(): Promise<File>;
@@ -37,13 +41,16 @@ interface FileSystemWritableFileStream extends WritableStream {
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>
} }
declare global { declare global {
interface Window { interface Window {
FileSystemDirectoryHandle
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle> showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>
} }

View File

@@ -1,6 +1,6 @@
import {fromByteArray as Base64Encode} from "base64-js"; import {fromByteArray as Base64Encode} from "base64-js";
export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis" export const IXAREA_API_ENDPOINT = "https://um-api.ixarea.com"
export interface UpdateInfo { export interface UpdateInfo {
Found: boolean Found: boolean

View File

@@ -144,9 +144,9 @@ export default {
} }
try { try {
this.dir = await window.showDirectoryPicker() this.dir = await window.showDirectoryPicker()
window.dir = this.dir const test_filename = "__unlock_music_write_test.txt"
window.f = await this.dir.getFileHandle("write-test.txt", {create: true}) await this.dir.getFileHandle(test_filename, {create: true})
await this.dir.removeEntry(test_filename)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }

View File

@@ -26,7 +26,8 @@
"dom", "dom",
"dom.iterable", "dom.iterable",
"scripthost" "scripthost"
] ],
"resolveJsonModule": true
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",