Skip to content

Commit

Permalink
Split out all file code to library
Browse files Browse the repository at this point in the history
  • Loading branch information
reinhrst committed Nov 28, 2023
1 parent 1a38962 commit e3138fe
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 279 deletions.
233 changes: 16 additions & 217 deletions src/convert/Convertor.tsx
Original file line number Diff line number Diff line change
@@ -1,206 +1,16 @@
import {Upload} from "./Upload.js"
import {FileTree, FileTreeBranch, FileTreeLeaf} from "./FileTree.js"
import {Upload} from "../lib/Upload.js"
import {FileTree, FileTreeBranch, readFileSystemHandle, updateLeaf, convertAll} from "../lib/FileTree.js"
import * as css from "./convertor.module.css"
import { JSX } from "preact"
import {useState} from 'preact/hooks'
import {convert, getOutputFilename} from "./ffmpeg.js"

const NR_WORKERS = 2

async function fromAsync<T>(source: Iterable<T> | AsyncIterable<T>): Promise<T[]> {
const items:T[] = [];
for await (const item of source) {
items.push(item);
}
return items
}

async function readFileSystemHandle(fshs: FileSystemHandle[], fileFilter: (file: File) => boolean): Promise<FileTreeBranch> {
const result: FileTreeBranch = new Map()
for (const fsh of fshs) {
if (fsh instanceof FileSystemFileHandle) {
const file = await fsh.getFile()
if (fileFilter(file)) {
result.set(fsh.name, {file: await fsh.getFile()})
}
} else if (fsh instanceof FileSystemDirectoryHandle) {
result.set(fsh.name, await readFileSystemHandle(
await fromAsync(fsh.values()), fileFilter))
} else {
throw new Error(`Unhandled case: ${fsh}`);
}
}
pruneDeadBranches(result)
return new Map(
[...result.entries()]
.sort(([a], [b]) => a.localeCompare(b, undefined, {numeric: true})))
}

function fileFilter(file: File, extension: string): boolean {
return !file.name.startsWith(".") && file.name.endsWith("." + extension)
}

function pruneDeadBranches(branch: FileTreeBranch) {
for (const [name, entry] of branch.entries()) {
if (entry instanceof Map) {
pruneDeadBranches(entry)
if (entry.size === 0) {
branch.delete(name)
}
}
}
}

function updateLeaf(files: FileTreeBranch, path: string[], update: FileTreeLeaf | ((current: FileTreeLeaf) => FileTreeLeaf)): FileTreeBranch {
const newFiles = new Map([...files])
const [p, ...restpath] = path
const item = files.get(p)
let newItem: FileTreeLeaf | FileTreeBranch
if (restpath.length === 0) {
if (!(item && "file" in item)) {
throw new Error(`Error in path: $(path}`)
}
if ("file" in update) {
newItem = update;
} else {
newItem = update(item)
}
} else {
if (!(item instanceof Map)) {
throw new Error(`Error in path: $(path}`)
}
newItem = updateLeaf(item, restpath, update)
}
newFiles.set(p, newItem)
return newFiles
}

function findLeaf(files: FileTreeBranch, path: string[]): FileTreeLeaf {
const [p, ...restpath] = path
const item = files.get(p)
if (restpath.length === 0) {
if (!(item && "file" in item)) {
throw new Error(`Error in path: $(path}`)
}
return item
}
if (!(item instanceof Map)) {
throw new Error(`Error in path: $(path}`)
}
return findLeaf(item, restpath)
}

function getAllLeafPaths(files: FileTreeBranch): string[][] {
return [...files.entries()]
.flatMap(([name, entry]) =>
(entry instanceof Map)
? getAllLeafPaths(entry).map(path => [name, ...path])
: [[name]])
}

async function nonEmptyFileExists(
directory: FileSystemDirectoryHandle,
path: string[]
): Promise<boolean> {
let pointer = directory
for (const p of path.slice(0, -1)) {
try {
pointer = await pointer.getDirectoryHandle(p)
} catch (e) {
if ((e as DOMException).name === "NotFoundError") {
return false
}
throw e
}
}
const filename = path.slice(-1)[0]
let file: FileSystemFileHandle
try {
file = await pointer.getFileHandle(filename)
} catch (e) {
if ((e as DOMException).name === "NotFoundError") {
return false
}
throw e
}
return (await file.getFile()).size > 0
}

async function convertOne(
files: FileTreeBranch,
path: string[],
destination: FileSystemDirectoryHandle,
setFiles: (cb: (files: FileTreeBranch) => FileTreeBranch) => void
) {
const leaf = findLeaf(files, path)
const outfilename = getOutputFilename(leaf.file.name)
const outpath = [...path.slice(0, -1), outfilename]
if (await nonEmptyFileExists(destination, outpath)) {
setFiles(files => updateLeaf(files, path, leaf => (
{file: leaf.file, progress: {"error": "File aready exists at the destination"}})))
return
}

let pointer = destination
for (const p of path.slice(0, -1)) {
//probably should catch if there is a file with this directoy name //TODO
pointer = await pointer.getDirectoryHandle(p, {create: true})
}
const outfile = await pointer.getFileHandle(outfilename, {create: true})
const outstream = await outfile.createWritable()
try {
await convert(leaf.file, outstream, (progress: FileTreeLeaf["progress"]) => {
setFiles(files =>
updateLeaf(files, path, leaf => ({file: leaf.file, progress})))
})
await outstream.close()
setFiles(files =>
updateLeaf(files, path, leaf => (
{file: leaf.file, progress: "done"})))
} catch (e) {
setFiles(files =>
updateLeaf(files, path, leaf => (
{file: leaf.file, progress: {error: `error while converting: $(e)`}})))
await outstream.close()
await pointer.removeEntry(outfilename)
}
}

async function convertAll(files: FileTreeBranch, setFiles: (cb: FileTreeBranch | ((files: FileTreeBranch) => FileTreeBranch)) => void) {
const destination = await window.showDirectoryPicker(
{id: "mp4save", mode: "readwrite"})
const outputFiles = ((await readFileSystemHandle([destination], file => fileFilter(file, "mp4"))).get(destination.name) ?? new Map()) as FileTreeBranch

const paths = getAllLeafPaths(files)
let newFiles = files
const queuedPaths: string[][] = []
for (const path of paths) {
const outpath = [...path.slice(0, -1), getOutputFilename(path.slice(-1)[0])]
if (await nonEmptyFileExists(destination, outpath)) {
newFiles = updateLeaf(
newFiles, path, leaf => ({file: leaf.file, progress: {"error": "File aready exists at the destination"}}))
continue
}
newFiles = updateLeaf(
newFiles, path, leaf => ({file: leaf.file, progress: "queue"}))
queuedPaths.push(path)
}
setFiles(newFiles)

const promises: Set<Promise<any>> = new Set()
let finished: Promise<any>[] = []
for (const path of queuedPaths) {
while (promises.size >= NR_WORKERS) {
await Promise.any(promises)
finished.forEach(p => promises.delete(p))
finished = []
}
const promise = convertOne(files, path, destination, setFiles)
promises.add(promise)
promise.then(() => finished.push(promise))
}
await Promise.all(promises)
}

export function Convertor({}: {}): JSX.Element {
const [files, setFiles] = useState<FileTreeBranch>(new Map())
Expand All @@ -210,30 +20,19 @@ export function Convertor({}: {}): JSX.Element {
const newFiles = await readFileSystemHandle(fileSystemHandles, file => fileFilter(file, "MTS"))
setFiles(files => new Map([...files, ...newFiles]))
}
function removeFile(name: string[]) {
setFiles(files => {
const newFiles = new Map([...files])
let pointer = newFiles
for (const dir of name.slice(0, -1)) {
if (!pointer.has(dir)) {
throw new Error(`Could not find ${dir} (${name})`)
}
const newpointer = pointer.get(dir)!
if (!(newpointer instanceof Map)) {
throw new Error(`${dir} (${name}) is a File`)
}
const copieddir = new Map([...newpointer])
pointer.set(dir, copieddir)
pointer = copieddir
}
const [filename] = name.slice(-1)
if (!pointer.has(filename)) {
throw new Error (`Could not find ${filename} (${name})`)
}
pointer.delete(filename)
pruneDeadBranches(newFiles)
return newFiles
})
function removeFile(path: string[]) {
setFiles(files => updateLeaf(files, path, null))
}

async function doConvertAll() {
setState("converting");
await convertAll(
files,
NR_WORKERS,
convert,
getOutputFilename,
setFiles)
setState("done")
}

return <>
Expand All @@ -246,7 +45,7 @@ export function Convertor({}: {}): JSX.Element {
</div>
{state === "uploading" && <Upload addFiles={addFiles} />}
<button disabled={!(state==="uploading" && files.size > 0)}
onClick={() => {setState("converting"); convertAll(files, setFiles).then(() => setState("done"))}}
onClick={doConvertAll}
>Start conversion</button>
</>
}
Expand Down
61 changes: 0 additions & 61 deletions src/convert/FileTree.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/convert/ffmpeg.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as LibAVTypes from '../../public/app/bundled/libavjs/dist/libav.types'
import type {FileTreeLeaf} from "./FileTree.js"
import type {FileTreeLeaf} from "../lib/FileTree.js"

declare global {
interface Window {
Expand Down
Loading

0 comments on commit e3138fe

Please sign in to comment.