forked from um/web
feature: directly write to fs
This commit is contained in:
parent
759c1bd87e
commit
21d5ae305c
@ -8,7 +8,27 @@
|
||||
multiple>
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||
<div slot="tip" class="el-upload__tip">本工具仅在浏览器内对文件进行解锁,无需消耗流量</div>
|
||||
<div slot="tip" class="el-upload__tip">
|
||||
<div>
|
||||
仅在浏览器内对文件进行解锁,无需消耗流量
|
||||
<el-tooltip effect="dark" placement="top-start">
|
||||
<div slot="content">
|
||||
算法在源代码中已经提供,所有运算都发生在本地
|
||||
</div>
|
||||
<i class="el-icon-info" style="font-size: 12px"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
工作模式: {{ parallel ? "多线程 Worker" : "单线程 Queue" }}
|
||||
<el-tooltip effect="dark" placement="top-start">
|
||||
<div slot="content">
|
||||
将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br/>
|
||||
从而更快的利用并行处理完成解锁
|
||||
</div>
|
||||
<i class="el-icon-info" style="font-size: 12px"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="el-fade-in"><!--todo: add delay to animation-->
|
||||
<el-progress
|
||||
v-show="progress_show" :format="progress_string" :percentage="progress_value"
|
||||
@ -30,7 +50,8 @@ export default {
|
||||
return {
|
||||
task_all: 0,
|
||||
task_finished: 0,
|
||||
queue: new DecryptQueue() // for http or file protocol
|
||||
queue: new DecryptQueue(), // for http or file protocol
|
||||
parallel: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -48,6 +69,7 @@ export default {
|
||||
() => spawn(new Worker('@/utils/worker.ts')),
|
||||
navigator.hardwareConcurrency || 1
|
||||
)
|
||||
this.parallel = true
|
||||
} else {
|
||||
console.log("Using Queue in Main Thread")
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DownloadBlobMusic, RemoveBlobMusic} from '@/utils/utils'
|
||||
import {RemoveBlobMusic} from '@/utils/utils'
|
||||
|
||||
export default {
|
||||
name: "PreviewTable",
|
||||
@ -60,7 +60,7 @@ export default {
|
||||
this.tableData.splice(index, 1);
|
||||
},
|
||||
handleDownload(row) {
|
||||
DownloadBlobMusic(row, this.policy)
|
||||
this.$emit("download", row)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export interface DecryptResult {
|
||||
ext: string
|
||||
|
||||
file: string
|
||||
blob: Blob
|
||||
picture?: string
|
||||
|
||||
message?: string
|
||||
|
@ -68,6 +68,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
ext,
|
||||
mime,
|
||||
title,
|
||||
|
@ -44,6 +44,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
mime,
|
||||
title,
|
||||
artist,
|
||||
|
@ -209,6 +209,7 @@ class NcmDecrypt {
|
||||
album: this.newMeta.album,
|
||||
picture: this.image?.url,
|
||||
file: URL.createObjectURL(this.blob),
|
||||
blob: this.blob as Blob,
|
||||
mime: this.mime
|
||||
}
|
||||
}
|
||||
|
@ -111,6 +111,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||
album: musicMeta.common.album,
|
||||
picture: imgUrl,
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
mime: mime
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string,
|
||||
album: tag.common.album,
|
||||
picture: GetCoverFromFile(tag),
|
||||
file: URL.createObjectURL(file),
|
||||
blob: file,
|
||||
mime: AudioMimeType[ext]
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
rawExt: "xm"
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ import {
|
||||
Table,
|
||||
TableColumn,
|
||||
Tooltip,
|
||||
Upload
|
||||
Upload,
|
||||
MessageBox
|
||||
} from 'element-ui';
|
||||
import 'element-ui/lib/theme-chalk/base.css';
|
||||
|
||||
@ -39,6 +40,7 @@ Vue.use(Radio);
|
||||
Vue.use(Tooltip);
|
||||
Vue.use(Progress);
|
||||
Vue.prototype.$notify = Notification;
|
||||
Vue.prototype.$confirm = MessageBox.confirm;
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
new Vue({
|
||||
|
51
src/shims-fs.d.ts
vendored
Normal file
51
src/shims-fs.d.ts
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
export interface FileSystemGetFileOptions {
|
||||
create?: boolean
|
||||
}
|
||||
|
||||
interface FileSystemCreateWritableOptions {
|
||||
keepExistingData?: boolean
|
||||
}
|
||||
|
||||
interface FileSystemFileHandle {
|
||||
getFile(): Promise<File>;
|
||||
|
||||
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>
|
||||
}
|
||||
|
||||
enum WriteCommandType {
|
||||
write = "write",
|
||||
seek = "seek",
|
||||
truncate = "truncate",
|
||||
}
|
||||
|
||||
interface WriteParams {
|
||||
type: WriteCommandType
|
||||
size?: number
|
||||
position?: number
|
||||
data: BufferSource | Blob | string
|
||||
}
|
||||
|
||||
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams
|
||||
|
||||
interface FileSystemWritableFileStream extends WritableStream {
|
||||
write(data: FileSystemWriteChunkType): Promise<undefined>
|
||||
|
||||
seek(position: number): Promise<undefined>
|
||||
|
||||
truncate(size: number): Promise<undefined>
|
||||
|
||||
close(): Promise<undefined> // should be implemented in WritableStream
|
||||
}
|
||||
|
||||
export declare interface FileSystemDirectoryHandle {
|
||||
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
FileSystemDirectoryHandle
|
||||
|
||||
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import {FileSystemDirectoryHandle} from "@/shims-fs";
|
||||
|
||||
export enum FilenamePolicy {
|
||||
ArtistAndTitle,
|
||||
@ -14,25 +15,39 @@ export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [
|
||||
{key: FilenamePolicy.SameAsOriginal, text: "同源文件名"},
|
||||
]
|
||||
|
||||
export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string {
|
||||
switch (policy) {
|
||||
case FilenamePolicy.TitleOnly:
|
||||
return `${data.title}.${data.ext}`;
|
||||
case FilenamePolicy.TitleAndArtist:
|
||||
return `${data.title} - ${data.artist}.${data.ext}`;
|
||||
case FilenamePolicy.SameAsOriginal:
|
||||
return `${data.rawFilename}.${data.ext}`;
|
||||
default:
|
||||
case FilenamePolicy.ArtistAndTitle:
|
||||
return `${data.artist} - ${data.title}.${data.ext}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) {
|
||||
let filename = GetDownloadFilename(data, policy)
|
||||
// prevent filename exist
|
||||
try {
|
||||
await dir.getFileHandle(filename)
|
||||
filename = `${new Date().getTime()} - ${filename}`
|
||||
} catch (e) {
|
||||
}
|
||||
const file = await dir.getFileHandle(filename, {create: true})
|
||||
const w = await file.createWritable()
|
||||
await w.write(data.blob)
|
||||
await w.close()
|
||||
|
||||
}
|
||||
|
||||
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
|
||||
const a = document.createElement('a');
|
||||
a.href = data.file;
|
||||
switch (policy) {
|
||||
default:
|
||||
case FilenamePolicy.ArtistAndTitle:
|
||||
a.download = data.artist + " - " + data.title + "." + data.ext;
|
||||
break;
|
||||
case FilenamePolicy.TitleOnly:
|
||||
a.download = data.title + "." + data.ext;
|
||||
break;
|
||||
case FilenamePolicy.TitleAndArtist:
|
||||
a.download = data.title + " - " + data.artist + "." + data.ext;
|
||||
break;
|
||||
case FilenamePolicy.SameAsOriginal:
|
||||
a.download = data.rawFilename + "." + data.ext;
|
||||
break;
|
||||
}
|
||||
a.download = GetDownloadFilename(data, policy)
|
||||
document.body.append(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
@ -26,15 +26,15 @@
|
||||
|
||||
<audio :autoplay="playing_auto" :src="playing_url" controls/>
|
||||
|
||||
<PreviewTable :policy="filename_policy" :table-data="tableData" @play="changePlaying"/>
|
||||
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import FileSelector from "../component/FileSelector"
|
||||
import PreviewTable from "../component/PreviewTable"
|
||||
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic} from "@/utils/utils"
|
||||
import FileSelector from "@/component/FileSelector"
|
||||
import PreviewTable from "@/component/PreviewTable"
|
||||
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile} from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
@ -44,19 +44,24 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeIndex: '1',
|
||||
tableData: [],
|
||||
playing_url: "",
|
||||
playing_auto: false,
|
||||
filename_policy: FilenamePolicy.ArtistAndTitle,
|
||||
instant_download: false,
|
||||
FilenamePolicies
|
||||
FilenamePolicies,
|
||||
dir: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
instant_download(val) {
|
||||
if (val) this.showDirectlySave()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showSuccess(data) {
|
||||
async showSuccess(data) {
|
||||
if (this.instant_download) {
|
||||
DownloadBlobMusic(data, this.filename_policy);
|
||||
await this.saveFile(data)
|
||||
RemoveBlobMusic(data);
|
||||
} else {
|
||||
this.tableData.push(data);
|
||||
@ -81,7 +86,7 @@ export default {
|
||||
duration: 6000
|
||||
});
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
window._paq.push(["trackEvent", "Error", errInfo, filename]);
|
||||
window._paq.push(["trackEvent", "Error", String(errInfo), filename]);
|
||||
}
|
||||
},
|
||||
changePlaying(url) {
|
||||
@ -98,12 +103,50 @@ export default {
|
||||
let index = 0;
|
||||
let c = setInterval(() => {
|
||||
if (index < this.tableData.length) {
|
||||
DownloadBlobMusic(this.tableData[index], this.filename_policy);
|
||||
this.saveFile(this.tableData[index])
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(c);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
|
||||
async saveFile(data) {
|
||||
if (this.dir) {
|
||||
await DirectlyWriteFile(data, this.filename_policy, this.dir)
|
||||
this.$notify({
|
||||
title: "保存成功",
|
||||
message: data.title,
|
||||
position: "top-left",
|
||||
type: "success",
|
||||
duration: 3000
|
||||
})
|
||||
} else {
|
||||
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()
|
||||
window.dir = this.dir
|
||||
window.f = await this.dir.getFileHandle("write-test.txt", {create: true})
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user