Add mergedrop: drag-and-drop video merger (ffmpeg + FastAPI + React)
This commit is contained in:
28
apps/mergedrop/build/README.md
Normal file
28
apps/mergedrop/build/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# MergeDrop image build
|
||||
|
||||
The Dockerfile and full source for the **MergeDrop** image live in a separate Gitea repo:
|
||||
|
||||
- Source: <https://git.alexzaw.dev/alexz/video-merger>
|
||||
- Published image: `git.alexzaw.dev/alexz/mergedrop:<tag>`
|
||||
|
||||
This folder holds only a thin helper script (`build.sh`) that clones the source
|
||||
repo and pushes the resulting image to the in-house Docker registry.
|
||||
|
||||
## Rebuild
|
||||
|
||||
```bash
|
||||
cd build/
|
||||
./build.sh # builds + pushes :latest and the version tag
|
||||
IMAGE_TAG=1.0.1 ./build.sh # override the tag
|
||||
```
|
||||
|
||||
Requires `docker login git.alexzaw.dev` to already be done (or `~/.docker/config.json`
|
||||
to have the registry credentials, which is the case on this host).
|
||||
|
||||
## Notes
|
||||
|
||||
- The image is a two-stage build (Node 20 → Python 3.11-slim) and produces an
|
||||
`amd64` image only. Add `--platform linux/arm64` to `build.sh` args to cross-build.
|
||||
- The application is a single FastAPI process that serves the static React build
|
||||
and a `/api/*` JSON API on the same port (`8000` inside the container).
|
||||
- `ffmpeg` is installed in the runtime image; no host-level ffmpeg required.
|
||||
37
apps/mergedrop/build/build.sh
Executable file
37
apps/mergedrop/build/build.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Build and push the MergeDrop Docker image.
|
||||
# Clones the source repo (or reuses an existing checkout) and tags/pushes
|
||||
# both :latest and the version tag.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/mergedrop}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-1.0.0}"
|
||||
SOURCE_REPO="${SOURCE_REPO:-https://git.alexzaw.dev/alexz/video-merger.git}"
|
||||
WORKDIR="${WORKDIR:-/tmp/mergedrop-build}"
|
||||
|
||||
echo "==> Preparing source at ${WORKDIR}"
|
||||
if [[ -d "${WORKDIR}/.git" ]]; then
|
||||
git -C "${WORKDIR}" fetch --quiet origin
|
||||
git -C "${WORKDIR}" reset --hard --quiet origin/HEAD
|
||||
else
|
||||
rm -rf "${WORKDIR}"
|
||||
git clone --quiet "${SOURCE_REPO}" "${WORKDIR}"
|
||||
fi
|
||||
|
||||
echo "==> Building ${IMAGE_NAME}:${IMAGE_TAG} (and :latest)"
|
||||
cd "${WORKDIR}"
|
||||
docker build "$@" \
|
||||
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
|
||||
-t "${IMAGE_NAME}:latest" \
|
||||
.
|
||||
|
||||
echo "==> Pushing ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo "==> Pushing ${IMAGE_NAME}:latest"
|
||||
docker push "${IMAGE_NAME}:latest"
|
||||
|
||||
echo ""
|
||||
echo "Done. To install on Runtipi:"
|
||||
echo " sudo /etc/runtipi/runtipi-cli appstore update"
|
||||
echo " sudo /etc/runtipi/runtipi-cli app install mergedrop:runtipi"
|
||||
43
apps/mergedrop/config.json
Normal file
43
apps/mergedrop/config.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "MergeDrop",
|
||||
"id": "mergedrop",
|
||||
"available": true,
|
||||
"short_desc": "Drag-and-drop video merger — drop clips, reorder, build one MP4.",
|
||||
"author": "alexz",
|
||||
"port": 8765,
|
||||
"categories": ["media", "utilities"],
|
||||
"description": "Self-hosted, drag-and-drop video merger. Upload multiple clips, reorder them, and produce a single normalized MP4 (h.264 + AAC) via ffmpeg. Handles mixed codecs, resolutions and framerates. Live progress streaming, in-browser preview, and downloadable result. No accounts, no telemetry, single container.",
|
||||
"tipi_version": 1,
|
||||
"version": "1.0.0",
|
||||
"source": "https://git.alexzaw.dev/alexz/video-merger",
|
||||
"website": "https://git.alexzaw.dev/alexz/video-merger",
|
||||
"exposable": true,
|
||||
"dynamic_config": true,
|
||||
"no_gui": false,
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Max files per merge",
|
||||
"default": "20",
|
||||
"required": false,
|
||||
"env_variable": "MERGEDROP_MAX_FILES"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Max per-file size (bytes)",
|
||||
"default": "2147483648",
|
||||
"required": false,
|
||||
"env_variable": "MERGEDROP_MAX_FILE_SIZE",
|
||||
"hint": "Default is 2 GiB (2147483648). Raise for larger source clips."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Output retention (hours)",
|
||||
"default": "24",
|
||||
"required": false,
|
||||
"env_variable": "MERGEDROP_OUTPUT_MAX_AGE_HOURS",
|
||||
"hint": "How long merged MP4s stay on disk before the janitor deletes them."
|
||||
}
|
||||
],
|
||||
"supported_architectures": ["amd64"]
|
||||
}
|
||||
47
apps/mergedrop/docker-compose.json
Normal file
47
apps/mergedrop/docker-compose.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
|
||||
"schemaVersion": 2,
|
||||
"services": [
|
||||
{
|
||||
"name": "mergedrop",
|
||||
"image": "git.alexzaw.dev/alexz/mergedrop:1.0.0",
|
||||
"isMain": true,
|
||||
"internalPort": 8000,
|
||||
"environment": [
|
||||
{ "key": "PORT", "value": "8000" },
|
||||
{ "key": "TZ", "value": "${TZ}" },
|
||||
{ "key": "MERGEDROP_UPLOAD_DIR", "value": "/data/uploads" },
|
||||
{ "key": "MERGEDROP_OUTPUT_DIR", "value": "/data/outputs" },
|
||||
{ "key": "MERGEDROP_THUMB_DIR", "value": "/data/thumbs" },
|
||||
{ "key": "MERGEDROP_MAX_FILES", "value": "${MERGEDROP_MAX_FILES}" },
|
||||
{ "key": "MERGEDROP_MAX_FILE_SIZE", "value": "${MERGEDROP_MAX_FILE_SIZE}" },
|
||||
{ "key": "MERGEDROP_OUTPUT_MAX_AGE_HOURS", "value": "${MERGEDROP_OUTPUT_MAX_AGE_HOURS}" },
|
||||
{ "key": "CORS_ORIGINS", "value": "*" }
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/uploads",
|
||||
"containerPath": "/data/uploads",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/outputs",
|
||||
"containerPath": "/data/outputs",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/thumbs",
|
||||
"containerPath": "/data/thumbs",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"healthCheck": {
|
||||
"test": "curl -fsS http://127.0.0.1:8000/api/ || exit 1",
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"retries": 3,
|
||||
"startPeriod": "20s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
46
apps/mergedrop/metadata/description.md
Normal file
46
apps/mergedrop/metadata/description.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# MergeDrop
|
||||
|
||||
A self-hosted, drag-and-drop **video merger**. Drop clips → reorder → click **Merge & Build MP4** → download a single ready-to-share file.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Most "merge video" tools are bloated desktop apps, paid SaaS, or sketchy converter sites that re-upload your footage to who-knows-where. MergeDrop is the opposite:
|
||||
|
||||
- 100% self-hosted, runs in a single container on your Runtipi
|
||||
- No accounts, no telemetry, no third-party upload caps
|
||||
- Mixed formats / resolutions / framerates → normalized to a clean **1080p30 h.264 + AAC** MP4
|
||||
|
||||
## Features
|
||||
|
||||
- Drag-and-drop upload, drag-to-reorder, drag-to-remove
|
||||
- Live ffmpeg progress (percentage + tail of stderr) per merge job
|
||||
- In-browser preview of the merged result, then one-click download
|
||||
- Built-in janitor — old outputs are auto-deleted after a configurable retention window
|
||||
|
||||
## Supported inputs
|
||||
|
||||
`mp4`, `mov`, `mkv`, `avi`, `webm`, `m4v`, `flv`, `wmv`, `mpg/mpeg` (mixed codecs and dimensions are normalized via two-pass ffmpeg pipeline).
|
||||
|
||||
## Output presets
|
||||
|
||||
`1080p` (default), `720p`, `4K`, `vertical 9:16`, `square 1:1` — selectable per job from the UI.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| Max files per merge | `20` | Hard cap per job |
|
||||
| Max per-file size | `2 GiB` | Per-upload limit (bytes) |
|
||||
| Output retention | `24h` | Janitor sweeps merged MP4s older than this |
|
||||
|
||||
## Volumes (persistent)
|
||||
|
||||
- `${APP_DATA_DIR}/data/uploads` — staged source clips
|
||||
- `${APP_DATA_DIR}/data/outputs` — merged MP4 results
|
||||
- `${APP_DATA_DIR}/data/thumbs` — generated upload thumbnails
|
||||
|
||||
## Tech stack
|
||||
|
||||
- Backend: FastAPI + `ffmpeg` subprocess (async, streamed progress)
|
||||
- Frontend: React 19 + Tailwind + `@dnd-kit` + `react-dropzone`
|
||||
- Single container, single port, no database
|
||||
Reference in New Issue
Block a user