Skip to content

Commit

Permalink
✨ add merge app
Browse files Browse the repository at this point in the history
  • Loading branch information
DominikPeters committed Sep 15, 2024
1 parent c3e5021 commit bff210d
Show file tree
Hide file tree
Showing 18 changed files with 2,065 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mockups/
node_modules/
node_modules/
libs/trash
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# mp3chapters.github.io

Online tool for adding chapters and other `id3` tags to audio files such as podcasts. Available at [mp3chapters.github.io](https://mp3chapters.github.io).
Online tool for adding chapters and other `id3` tags to audio files such as podcasts. Available at [mp3chapters.github.io](https://mp3chapters.github.io). It also features a [tool for merging audio files](https://mp3chapters.github.io/merge/).

Uses just HTML/CSS and vanilla JS. No server-side code, so it can be run by just starting a webserver in the repo directory (e.g. `python3 -m http.server`).
Uses just HTML/CSS and vanilla JS. No server-side code, so it can be run by just starting a webserver in the repo directory (e.g. `python3 -m http.server`). To avoid duplicating images, the tool uses hashes made with subtlecrypto. This feature is only available in https and `localhost`.

Built using [node-id3](https://github.com/Zazama/node-id3), [browserify](https://browserify.org/), [wavesurfer.js](https://wavesurfer-js.org/), and [Vidstack Player](https://vidstack.io/docs).
Built using [node-id3](https://github.com/Zazama/node-id3), [browserify](https://browserify.org/), [wavesurfer.js](https://wavesurfer-js.org/), and [Vidstack Player](https://vidstack.io/docs). The merge app uses [ffmpeg.wasm](https://ffmpegwasm.netlify.app/).

Feedback, bug reports, and pull requests are very welcome.

Expand Down
9 changes: 6 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,12 @@
d="M0 .5A.5.5 0 0 1 .5 0h2a.5.5 0 0 1 0 1h-2A.5.5 0 0 1 0 .5m4 0a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1h-10A.5.5 0 0 1 4 .5m-4 2A.5.5 0 0 1 .5 2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-4 2A.5.5 0 0 1 .5 4h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5m-4 2A.5.5 0 0 1 .5 6h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5m-4 2A.5.5 0 0 1 .5 8h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5m-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1h-10a.5.5 0 0 1-.5-.5m-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5m4 0a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5" />
</svg>
<span class="d-none d-sm-inline">Online</span> MP3 Podcast Chapter Editor</a>
<span class="d-none d-md-inline" style="margin-left: -5px; font-size: var(--bs-navbar-brand-font-size);">|
&nbsp;<a href="/player" class="text-dark-emphasis"
data-tippy-content="Desktop app for playing MP3 with chapters">Player</a></span>
<span class="d-none d-md-inline" style="margin-left: -5px; font-size: var(--bs-navbar-brand-font-size);">
|&nbsp;<a href="/merge" class="text-dark-emphasis"
data-tippy-content="Online app for combining several MP3 files into one">File Merger</a>
|&nbsp;<a href="/player" class="text-dark-emphasis"
data-tippy-content="Desktop app for playing MP3 with chapters">Player</a>
</span>
<a href="https://github.com/mp3chapters/mp3chapters.github.io" class="icon-link ms-auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github"
viewBox="0 0 16 16" aria-hidden="true">
Expand Down
12 changes: 12 additions & 0 deletions libs/ffmpeg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This is https://ffmpegwasm.netlify.app/
Used for finding the duration of an mp3 file, and for merging mp3 files (either copying or re-encoding).

Built to only include what is necessary for mp3 processing, with the changes made in the repo:
https://github.com/mp3chapters/ffmpeg.wasm/
(run `make prd`)

ESM modules bundled into single files using command
```bash
esbuild index.js --bundle --outfile=bundle.js --format=esm
```
for `ffmpeg/classes.js`, `ffmpeg/worker.js`, and `util/index.js`
16 changes: 16 additions & 0 deletions libs/ffmpeg/ffmpeg-core.js

Large diffs are not rendered by default.

Binary file added libs/ffmpeg/ffmpeg-core.wasm
Binary file not shown.
279 changes: 279 additions & 0 deletions libs/ffmpeg/ffmpeg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// const.js
var CORE_VERSION = "0.12.1";
var CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
var FFMessageType;
(function(FFMessageType2) {
FFMessageType2["LOAD"] = "LOAD";
FFMessageType2["EXEC"] = "EXEC";
FFMessageType2["WRITE_FILE"] = "WRITE_FILE";
FFMessageType2["READ_FILE"] = "READ_FILE";
FFMessageType2["DELETE_FILE"] = "DELETE_FILE";
FFMessageType2["RENAME"] = "RENAME";
FFMessageType2["CREATE_DIR"] = "CREATE_DIR";
FFMessageType2["LIST_DIR"] = "LIST_DIR";
FFMessageType2["DELETE_DIR"] = "DELETE_DIR";
FFMessageType2["ERROR"] = "ERROR";
FFMessageType2["DOWNLOAD"] = "DOWNLOAD";
FFMessageType2["PROGRESS"] = "PROGRESS";
FFMessageType2["LOG"] = "LOG";
FFMessageType2["MOUNT"] = "MOUNT";
FFMessageType2["UNMOUNT"] = "UNMOUNT";
})(FFMessageType || (FFMessageType = {}));

// utils.js
var getMessageID = /* @__PURE__ */ (() => {
let messageID = 0;
return () => messageID++;
})();

// errors.js
var ERROR_UNKNOWN_MESSAGE_TYPE = new Error("unknown message type");
var ERROR_NOT_LOADED = new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first");
var ERROR_TERMINATED = new Error("called FFmpeg.terminate()");
var ERROR_IMPORT_FAILURE = new Error("failed to import ffmpeg-core.js");

// classes.js
var FFmpeg = class {
#worker = null;
/**
* #resolves and #rejects tracks Promise resolves and rejects to
* be called when we receive message from web worker.
*/
#resolves = {};
#rejects = {};
#logEventCallbacks = [];
#progressEventCallbacks = [];
loaded = false;
/**
* register worker message event handlers.
*/
#registerHandlers = () => {
if (this.#worker) {
this.#worker.onmessage = ({ data: { id, type, data } }) => {
switch (type) {
case FFMessageType.LOAD:
this.loaded = true;
this.#resolves[id](data);
break;
case FFMessageType.MOUNT:
case FFMessageType.UNMOUNT:
case FFMessageType.EXEC:
case FFMessageType.WRITE_FILE:
case FFMessageType.READ_FILE:
case FFMessageType.DELETE_FILE:
case FFMessageType.RENAME:
case FFMessageType.CREATE_DIR:
case FFMessageType.LIST_DIR:
case FFMessageType.DELETE_DIR:
this.#resolves[id](data);
break;
case FFMessageType.LOG:
this.#logEventCallbacks.forEach((f) => f(data));
break;
case FFMessageType.PROGRESS:
this.#progressEventCallbacks.forEach((f) => f(data));
break;
case FFMessageType.ERROR:
this.#rejects[id](data);
break;
}
delete this.#resolves[id];
delete this.#rejects[id];
};
}
};
/**
* Generic function to send messages to web worker.
*/
#send = ({ type, data }, trans = [], signal) => {
if (!this.#worker) {
return Promise.reject(ERROR_NOT_LOADED);
}
return new Promise((resolve, reject) => {
const id = getMessageID();
this.#worker && this.#worker.postMessage({ id, type, data }, trans);
this.#resolves[id] = resolve;
this.#rejects[id] = reject;
signal?.addEventListener("abort", () => {
reject(new DOMException(`Message # ${id} was aborted`, "AbortError"));
}, { once: true });
});
};
on(event, callback) {
if (event === "log") {
this.#logEventCallbacks.push(callback);
} else if (event === "progress") {
this.#progressEventCallbacks.push(callback);
}
}
off(event, callback) {
if (event === "log") {
this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback);
} else if (event === "progress") {
this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback);
}
}
/**
* Loads ffmpeg-core inside web worker. It is required to call this method first
* as it initializes WebAssembly and other essential variables.
*
* @category FFmpeg
* @returns `true` if ffmpeg core is loaded for the first time.
*/
load = (config = {}, { signal } = {}) => {
if (!this.#worker) {
this.#worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module"
});
this.#registerHandlers();
}
return this.#send({
type: FFMessageType.LOAD,
data: config
}, void 0, signal);
};
/**
* Execute ffmpeg command.
*
* @remarks
* To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
* by default.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* await ffmpeg.writeFile("video.avi", ...);
* // ffmpeg -i video.avi video.mp4
* await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
* const data = ffmpeg.readFile("video.mp4");
* ```
*
* @returns `0` if no error, `!= 0` if timeout (1) or error.
* @category FFmpeg
*/
exec = (args, timeout = -1, { signal } = {}) => this.#send({
type: FFMessageType.EXEC,
data: { args, timeout }
}, void 0, signal);
/**
* Terminate all ongoing API calls and terminate web worker.
* `FFmpeg.load()` must be called again before calling any other APIs.
*
* @category FFmpeg
*/
terminate = () => {
const ids = Object.keys(this.#rejects);
for (const id of ids) {
this.#rejects[id](ERROR_TERMINATED);
delete this.#rejects[id];
delete this.#resolves[id];
}
if (this.#worker) {
this.#worker.terminate();
this.#worker = null;
this.loaded = false;
}
};
/**
* Write data to ffmpeg.wasm.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
* await ffmpeg.writeFile("text.txt", "hello world");
* ```
*
* @category File System
*/
writeFile = (path, data, { signal } = {}) => {
const trans = [];
if (data instanceof Uint8Array) {
trans.push(data.buffer);
}
return this.#send({
type: FFMessageType.WRITE_FILE,
data: { path, data }
}, trans, signal);
};
mount = (fsType, options, mountPoint) => {
const trans = [];
return this.#send({
type: FFMessageType.MOUNT,
data: { fsType, options, mountPoint }
}, trans);
};
unmount = (mountPoint) => {
const trans = [];
return this.#send({
type: FFMessageType.UNMOUNT,
data: { mountPoint }
}, trans);
};
/**
* Read data from ffmpeg.wasm.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* const data = await ffmpeg.readFile("video.mp4");
* ```
*
* @category File System
*/
readFile = (path, encoding = "binary", { signal } = {}) => this.#send({
type: FFMessageType.READ_FILE,
data: { path, encoding }
}, void 0, signal);
/**
* Delete a file.
*
* @category File System
*/
deleteFile = (path, { signal } = {}) => this.#send({
type: FFMessageType.DELETE_FILE,
data: { path }
}, void 0, signal);
/**
* Rename a file or directory.
*
* @category File System
*/
rename = (oldPath, newPath, { signal } = {}) => this.#send({
type: FFMessageType.RENAME,
data: { oldPath, newPath }
}, void 0, signal);
/**
* Create a directory.
*
* @category File System
*/
createDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.CREATE_DIR,
data: { path }
}, void 0, signal);
/**
* List directory contents.
*
* @category File System
*/
listDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.LIST_DIR,
data: { path }
}, void 0, signal);
/**
* Delete an empty directory.
*
* @category File System
*/
deleteDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.DELETE_DIR,
data: { path }
}, void 0, signal);
};
export {
FFmpeg
};
Loading

0 comments on commit bff210d

Please sign in to comment.