forked from um/web
feature: directly write to fs
This commit is contained in:
parent
759c1bd87e
commit
21d5ae305c
@ -8,7 +8,27 @@
|
|||||||
multiple>
|
multiple>
|
||||||
<i class="el-icon-upload"/>
|
<i class="el-icon-upload"/>
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
<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-->
|
<transition name="el-fade-in"><!--todo: add delay to animation-->
|
||||||
<el-progress
|
<el-progress
|
||||||
v-show="progress_show" :format="progress_string" :percentage="progress_value"
|
v-show="progress_show" :format="progress_string" :percentage="progress_value"
|
||||||
@ -30,7 +50,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
task_all: 0,
|
task_all: 0,
|
||||||
task_finished: 0,
|
task_finished: 0,
|
||||||
queue: new DecryptQueue() // for http or file protocol
|
queue: new DecryptQueue(), // for http or file protocol
|
||||||
|
parallel: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -48,6 +69,7 @@ export default {
|
|||||||
() => spawn(new Worker('@/utils/worker.ts')),
|
() => spawn(new Worker('@/utils/worker.ts')),
|
||||||
navigator.hardwareConcurrency || 1
|
navigator.hardwareConcurrency || 1
|
||||||
)
|
)
|
||||||
|
this.parallel = true
|
||||||
} else {
|
} else {
|
||||||
console.log("Using Queue in Main Thread")
|
console.log("Using Queue in Main Thread")
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {DownloadBlobMusic, RemoveBlobMusic} from '@/utils/utils'
|
import {RemoveBlobMusic} from '@/utils/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PreviewTable",
|
name: "PreviewTable",
|
||||||
@ -60,7 +60,7 @@ export default {
|
|||||||
this.tableData.splice(index, 1);
|
this.tableData.splice(index, 1);
|
||||||
},
|
},
|
||||||
handleDownload(row) {
|
handleDownload(row) {
|
||||||
DownloadBlobMusic(row, this.policy)
|
this.$emit("download", row)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ export interface DecryptResult {
|
|||||||
ext: string
|
ext: string
|
||||||
|
|
||||||
file: string
|
file: string
|
||||||
|
blob: Blob
|
||||||
picture?: string
|
picture?: string
|
||||||
|
|
||||||
message?: string
|
message?: string
|
||||||
|
@ -68,6 +68,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
file: URL.createObjectURL(musicBlob),
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
blob: musicBlob,
|
||||||
ext,
|
ext,
|
||||||
mime,
|
mime,
|
||||||
title,
|
title,
|
||||||
|
@ -44,6 +44,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
|
|||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
file: URL.createObjectURL(musicBlob),
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
blob: musicBlob,
|
||||||
mime,
|
mime,
|
||||||
title,
|
title,
|
||||||
artist,
|
artist,
|
||||||
|
@ -209,6 +209,7 @@ class NcmDecrypt {
|
|||||||
album: this.newMeta.album,
|
album: this.newMeta.album,
|
||||||
picture: this.image?.url,
|
picture: this.image?.url,
|
||||||
file: URL.createObjectURL(this.blob),
|
file: URL.createObjectURL(this.blob),
|
||||||
|
blob: this.blob as Blob,
|
||||||
mime: this.mime
|
mime: this.mime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: imgUrl,
|
picture: imgUrl,
|
||||||
file: URL.createObjectURL(musicBlob),
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
blob: musicBlob,
|
||||||
mime: mime
|
mime: mime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string,
|
|||||||
album: tag.common.album,
|
album: tag.common.album,
|
||||||
picture: GetCoverFromFile(tag),
|
picture: GetCoverFromFile(tag),
|
||||||
file: URL.createObjectURL(file),
|
file: URL.createObjectURL(file),
|
||||||
|
blob: file,
|
||||||
mime: AudioMimeType[ext]
|
mime: AudioMimeType[ext]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
file: URL.createObjectURL(musicBlob),
|
file: URL.createObjectURL(musicBlob),
|
||||||
|
blob: musicBlob,
|
||||||
rawExt: "xm"
|
rawExt: "xm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,8 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Upload
|
Upload,
|
||||||
|
MessageBox
|
||||||
} from 'element-ui';
|
} from 'element-ui';
|
||||||
import 'element-ui/lib/theme-chalk/base.css';
|
import 'element-ui/lib/theme-chalk/base.css';
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ Vue.use(Radio);
|
|||||||
Vue.use(Tooltip);
|
Vue.use(Tooltip);
|
||||||
Vue.use(Progress);
|
Vue.use(Progress);
|
||||||
Vue.prototype.$notify = Notification;
|
Vue.prototype.$notify = Notification;
|
||||||
|
Vue.prototype.$confirm = MessageBox.confirm;
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
new Vue({
|
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 {DecryptResult} from "@/decrypt/entity";
|
||||||
|
import {FileSystemDirectoryHandle} from "@/shims-fs";
|
||||||
|
|
||||||
export enum FilenamePolicy {
|
export enum FilenamePolicy {
|
||||||
ArtistAndTitle,
|
ArtistAndTitle,
|
||||||
@ -14,25 +15,39 @@ export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [
|
|||||||
{key: FilenamePolicy.SameAsOriginal, text: "同源文件名"},
|
{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) {
|
export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = data.file;
|
a.href = data.file;
|
||||||
switch (policy) {
|
a.download = GetDownloadFilename(data, 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;
|
|
||||||
}
|
|
||||||
document.body.append(a);
|
document.body.append(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
|
@ -26,15 +26,15 @@
|
|||||||
|
|
||||||
<audio :autoplay="playing_auto" :src="playing_url" controls/>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import FileSelector from "../component/FileSelector"
|
import FileSelector from "@/component/FileSelector"
|
||||||
import PreviewTable from "../component/PreviewTable"
|
import PreviewTable from "@/component/PreviewTable"
|
||||||
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic} from "@/utils/utils"
|
import {DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile} from "@/utils/utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@ -44,19 +44,24 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeIndex: '1',
|
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: "",
|
playing_url: "",
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
filename_policy: FilenamePolicy.ArtistAndTitle,
|
filename_policy: FilenamePolicy.ArtistAndTitle,
|
||||||
instant_download: false,
|
instant_download: false,
|
||||||
FilenamePolicies
|
FilenamePolicies,
|
||||||
|
dir: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
instant_download(val) {
|
||||||
|
if (val) this.showDirectlySave()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showSuccess(data) {
|
async showSuccess(data) {
|
||||||
if (this.instant_download) {
|
if (this.instant_download) {
|
||||||
DownloadBlobMusic(data, this.filename_policy);
|
await this.saveFile(data)
|
||||||
RemoveBlobMusic(data);
|
RemoveBlobMusic(data);
|
||||||
} else {
|
} else {
|
||||||
this.tableData.push(data);
|
this.tableData.push(data);
|
||||||
@ -81,7 +86,7 @@ export default {
|
|||||||
duration: 6000
|
duration: 6000
|
||||||
});
|
});
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
window._paq.push(["trackEvent", "Error", errInfo, filename]);
|
window._paq.push(["trackEvent", "Error", String(errInfo), filename]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePlaying(url) {
|
changePlaying(url) {
|
||||||
@ -98,12 +103,50 @@ export default {
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
let c = setInterval(() => {
|
let c = setInterval(() => {
|
||||||
if (index < this.tableData.length) {
|
if (index < this.tableData.length) {
|
||||||
DownloadBlobMusic(this.tableData[index], this.filename_policy);
|
this.saveFile(this.tableData[index])
|
||||||
index++;
|
index++;
|
||||||
} else {
|
} else {
|
||||||
clearInterval(c);
|
clearInterval(c);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 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