feat(peertube): Add video import with multi-track subtitle sync
- New peertube-import script for importing from YouTube, Vimeo, 1000+ sites - CGI endpoints for portal integration (peertube-import, peertube-import-status) - Portal UI: Video Import card with progress tracking - Multi-language subtitle download and PeerTube caption upload - Fixed stdout/stderr separation for reliable function returns - UCI config: uses peertube.admin.username/password - Package version bumped to 1.2.0 - Added README.md with full documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e78f4fdf3d
commit
bbf2b19415
@ -3045,3 +3045,46 @@ git checkout HEAD -- index.html
|
||||
- YouTube videos blocked by PO token requirement for subtitle access
|
||||
- PeerTube videos on tube.gk2 have no captions uploaded
|
||||
- Metadata extraction works; transcript step fails without subtitles/Whisper
|
||||
|
||||
31. **PeerTube Video Import with Multi-Track Subtitles (2026-02-21)**
|
||||
- New `peertube-import` CLI tool for importing videos from YouTube, Vimeo, and 1000+ sites.
|
||||
- **Features:**
|
||||
- Download video via yt-dlp (best quality MP4)
|
||||
- Extract metadata (title, description, tags)
|
||||
- Download subtitles in multiple languages (configurable)
|
||||
- Upload video to PeerTube via API
|
||||
- Upload each subtitle track via `/api/v1/videos/{id}/captions/{lang}`
|
||||
- **CLI Interface:**
|
||||
```bash
|
||||
peertube-import --lang fr,en,de,es https://youtube.com/watch?v=xxx
|
||||
peertube-import --privacy 2 --channel 1 https://vimeo.com/xxx
|
||||
```
|
||||
- **Portal Integration:**
|
||||
- New "Video Import" card in Intelligence & Analyse section
|
||||
- Modal dialog with URL input, language selection, privacy options
|
||||
- Progress bar with live status updates
|
||||
- Direct link to imported video on completion
|
||||
- **CGI Endpoints:**
|
||||
- `POST /cgi-bin/peertube-import` — Start import job
|
||||
- `GET /cgi-bin/peertube-import-status?job_id=xxx` — Poll status
|
||||
- **Authentication:**
|
||||
- Supports PEERTUBE_TOKEN env var
|
||||
- UCI config: `peertube.api.username` / `peertube.api.password`
|
||||
- OAuth client credential flow for token acquisition
|
||||
- Package version bumped to 1.2.0
|
||||
- **Files:**
|
||||
- `secubox-app-peertube/files/usr/sbin/peertube-import` (new)
|
||||
- `secubox-app-peertube/files/www/cgi-bin/peertube-import` (new)
|
||||
- `secubox-app-peertube/files/www/cgi-bin/peertube-import-status` (new)
|
||||
- `luci-app-secubox-portal/root/www/gk2-hub/portal.html` (updated)
|
||||
- `secubox-app-peertube/Makefile` (updated)
|
||||
|
||||
31. **PeerTube Import Fixes (2026-02-21)**
|
||||
- Fixed stdout/stderr separation in `peertube-import` script
|
||||
- Changed UCI config path from `peertube.api.*` to `peertube.admin.*`
|
||||
- Fixed yt-dlp output redirection to prevent mixing with function return values
|
||||
- Fixed curl response handling in upload functions (use temp file, not 2>&1)
|
||||
- Upgraded yt-dlp to 2026.2.4 for YouTube compatibility
|
||||
- Installed Node.js (20.20.0) for yt-dlp JavaScript runtime support
|
||||
- Verified end-to-end import flow: YouTube → download → subtitles → PeerTube upload
|
||||
|
||||
|
||||
@ -389,7 +389,12 @@
|
||||
"Bash(__NEW_LINE_02bd2dd51e90cbf8__ echo \"\")",
|
||||
"Bash(__NEW_LINE_70eb6f3ae1c26753__ echo \"\")",
|
||||
"WebFetch(domain:radio.gk2.secubox.in)",
|
||||
"WebFetch(domain:nextcloud-talk.readthedocs.io)"
|
||||
"WebFetch(domain:nextcloud-talk.readthedocs.io)",
|
||||
"Bash(__NEW_LINE_0334b7e65952251f__ rm -f \"$COOKIES\")",
|
||||
"Bash(__NEW_LINE_d0f84baac9f3813d__ rm -f \"$COOKIES\")",
|
||||
"Bash(__NEW_LINE_722c25da6bf58fe1__ rm -f \"$COOKIES\" /tmp/login.html)",
|
||||
"WebFetch(domain:portal.nextcloud.com)",
|
||||
"WebFetch(domain:arnowelzel.de)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,7 @@
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.service-card:hover {
|
||||
border-color: var(--accent);
|
||||
@ -126,6 +127,101 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer a { color: var(--accent); text-decoration: none; }
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 20px 60px rgba(0,95,158,0.3);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.modal-header h3 { margin: 0; color: #fff; }
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-close:hover { color: #fff; }
|
||||
.modal-body { padding: 20px; }
|
||||
.modal-footer {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.form-group { margin-bottom: 15px; }
|
||||
.form-group label { display: block; margin-bottom: 5px; color: var(--text); font-size: 0.9rem; }
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.modal-input:focus { outline: none; border-color: var(--accent); }
|
||||
.checkbox-group { display: flex; flex-wrap: wrap; gap: 15px; }
|
||||
.checkbox-label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
|
||||
.checkbox-label input { accent-color: var(--accent); }
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover { background: #007acc; }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-secondary:hover { border-color: var(--accent); }
|
||||
.progress-bar {
|
||||
background: var(--bg);
|
||||
border-radius: 4px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.progress-fill {
|
||||
background: var(--accent);
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.progress-status { font-size: 0.85rem; color: var(--muted); }
|
||||
.result-success { color: #2ecc71; padding: 15px; background: rgba(46,204,113,0.1); border-radius: 6px; }
|
||||
.result-success a { color: var(--accent); }
|
||||
.result-error { color: #e74c3c; padding: 15px; background: rgba(231,76,60,0.1); border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -203,15 +299,25 @@
|
||||
<h2 class="section-title">Intelligence & Analyse</h2>
|
||||
<div class="services-grid">
|
||||
<a href="https://analyse.gk2.secubox.in/" class="service-card" target="_blank">
|
||||
<div class="service-icon">🎬</div>
|
||||
<div class="service-icon">🔍</div>
|
||||
<div class="service-name">PeerTube Analyse</div>
|
||||
<div class="service-url">analyse.gk2.secubox.in</div>
|
||||
</a>
|
||||
<a href="#" class="service-card" id="importVideoCard" onclick="openImportModal(); return false;">
|
||||
<div class="service-icon">📥</div>
|
||||
<div class="service-name">Video Import</div>
|
||||
<div class="service-url">YouTube, Vimeo, 1000+ sites</div>
|
||||
</a>
|
||||
<a href="https://stream.gk2.secubox.in/" class="service-card" target="_blank">
|
||||
<div class="service-icon">📻</div>
|
||||
<div class="service-name">Radio Stream</div>
|
||||
<div class="service-url">stream.gk2.secubox.in</div>
|
||||
</a>
|
||||
<a href="https://radio.gk2.secubox.in/" class="service-card" target="_blank">
|
||||
<div class="service-icon">🎵</div>
|
||||
<div class="service-name">Lyrion Radio</div>
|
||||
<div class="service-url">radio.gk2.secubox.in</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Administration</h2>
|
||||
@ -243,6 +349,50 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Video Import Modal -->
|
||||
<div class="modal-overlay" id="importModal" style="display:none;">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<h3>📥 Video Import</h3>
|
||||
<button class="modal-close" onclick="closeImportModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Video URL</label>
|
||||
<input type="text" id="importUrl" placeholder="https://youtube.com/watch?v=..." class="modal-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Subtitle Languages</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-label"><input type="checkbox" value="fr" checked> Francais</label>
|
||||
<label class="checkbox-label"><input type="checkbox" value="en" checked> English</label>
|
||||
<label class="checkbox-label"><input type="checkbox" value="de"> Deutsch</label>
|
||||
<label class="checkbox-label"><input type="checkbox" value="es"> Espanol</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Privacy</label>
|
||||
<select id="importPrivacy" class="modal-input">
|
||||
<option value="1">Public</option>
|
||||
<option value="2">Unlisted</option>
|
||||
<option value="3">Private</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="importProgress" style="display:none;">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="progress-status" id="progressStatus">Starting...</div>
|
||||
</div>
|
||||
<div id="importResult" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeImportModal()">Cancel</button>
|
||||
<button class="btn-primary" id="startImportBtn" onclick="startImport()">Import Video</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check authentication
|
||||
var token = sessionStorage.getItem("secubox_token");
|
||||
@ -259,6 +409,131 @@ document.getElementById("logoutBtn").onclick = function() {
|
||||
sessionStorage.clear();
|
||||
window.location.href = "/login.html";
|
||||
};
|
||||
|
||||
// Video Import Modal
|
||||
var importJobId = null;
|
||||
var pollInterval = null;
|
||||
|
||||
function openImportModal() {
|
||||
document.getElementById("importModal").style.display = "flex";
|
||||
document.getElementById("importUrl").value = "";
|
||||
document.getElementById("importProgress").style.display = "none";
|
||||
document.getElementById("importResult").style.display = "none";
|
||||
document.getElementById("startImportBtn").disabled = false;
|
||||
}
|
||||
|
||||
function closeImportModal() {
|
||||
document.getElementById("importModal").style.display = "none";
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedLanguages() {
|
||||
var checkboxes = document.querySelectorAll('.checkbox-group input:checked');
|
||||
var langs = [];
|
||||
checkboxes.forEach(function(cb) { langs.push(cb.value); });
|
||||
return langs.length > 0 ? langs : ["fr", "en"];
|
||||
}
|
||||
|
||||
function startImport() {
|
||||
var url = document.getElementById("importUrl").value.trim();
|
||||
if (!url) {
|
||||
alert("Please enter a video URL");
|
||||
return;
|
||||
}
|
||||
|
||||
var languages = getSelectedLanguages();
|
||||
var privacy = document.getElementById("importPrivacy").value;
|
||||
|
||||
document.getElementById("startImportBtn").disabled = true;
|
||||
document.getElementById("importProgress").style.display = "block";
|
||||
document.getElementById("importResult").style.display = "none";
|
||||
document.getElementById("progressFill").style.width = "5%";
|
||||
document.getElementById("progressStatus").textContent = "Starting import...";
|
||||
|
||||
fetch("/cgi-bin/peertube-import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
languages: languages,
|
||||
privacy: parseInt(privacy)
|
||||
})
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
showImportError(data.error);
|
||||
return;
|
||||
}
|
||||
importJobId = data.job_id;
|
||||
pollImportStatus();
|
||||
})
|
||||
.catch(function(err) {
|
||||
showImportError("Failed to start import: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function pollImportStatus() {
|
||||
if (!importJobId) return;
|
||||
|
||||
pollInterval = setInterval(function() {
|
||||
fetch("/cgi-bin/peertube-import-status?job_id=" + importJobId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
clearInterval(pollInterval);
|
||||
showImportError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = data.progress || 0;
|
||||
var status = data.status || "unknown";
|
||||
document.getElementById("progressFill").style.width = progress + "%";
|
||||
|
||||
if (status === "downloading") {
|
||||
document.getElementById("progressStatus").textContent = "Downloading video and subtitles...";
|
||||
} else if (status === "uploading") {
|
||||
document.getElementById("progressStatus").textContent = "Uploading to PeerTube...";
|
||||
} else if (status === "completed") {
|
||||
clearInterval(pollInterval);
|
||||
showImportSuccess(data);
|
||||
} else if (status === "failed") {
|
||||
clearInterval(pollInterval);
|
||||
showImportError(data.error || "Import failed");
|
||||
} else {
|
||||
document.getElementById("progressStatus").textContent = status;
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
document.getElementById("progressStatus").textContent = "Checking status...";
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showImportSuccess(data) {
|
||||
document.getElementById("importProgress").style.display = "none";
|
||||
document.getElementById("importResult").style.display = "block";
|
||||
document.getElementById("importResult").className = "result-success";
|
||||
|
||||
var videoUrl = data.video_url || ("https://tube.gk2.secubox.in/w/" + data.video_uuid);
|
||||
var title = data.title || "Imported Video";
|
||||
|
||||
document.getElementById("importResult").innerHTML =
|
||||
'<strong>Import successful!</strong><br><br>' +
|
||||
'Title: ' + title + '<br>' +
|
||||
'<a href="' + videoUrl + '" target="_blank">Open video on PeerTube</a>';
|
||||
}
|
||||
|
||||
function showImportError(message) {
|
||||
document.getElementById("importProgress").style.display = "none";
|
||||
document.getElementById("importResult").style.display = "block";
|
||||
document.getElementById("importResult").className = "result-error";
|
||||
document.getElementById("importResult").innerHTML = '<strong>Import failed</strong><br>' + message;
|
||||
document.getElementById("startImportBtn").disabled = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=secubox-app-peertube
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_VERSION:=1.2.0
|
||||
PKG_ARCH:=all
|
||||
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
|
||||
PKG_LICENSE:=AGPL-3.0
|
||||
@ -23,6 +23,7 @@ PeerTube federated video streaming platform.
|
||||
Runs in an LXC Debian container with PostgreSQL, Redis, and Node.js.
|
||||
Supports video hosting, live streaming, and ActivityPub federation.
|
||||
Includes peertube-analyse: transcript extraction and Claude AI analysis.
|
||||
Includes peertube-import: auto-import from YouTube/Vimeo with multi-track subtitles.
|
||||
endef
|
||||
|
||||
define Package/secubox-app-peertube/conffiles
|
||||
@ -42,6 +43,7 @@ define Package/secubox-app-peertube/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/peertubectl $(1)/usr/sbin/peertubectl
|
||||
$(INSTALL_BIN) ./files/usr/sbin/peertube-analyse $(1)/usr/sbin/peertube-analyse
|
||||
$(INSTALL_BIN) ./files/usr/sbin/peertube-import $(1)/usr/sbin/peertube-import
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/peertube-analyse
|
||||
$(INSTALL_DATA) ./files/www/peertube-analyse/index.html $(1)/www/peertube-analyse/
|
||||
@ -49,6 +51,8 @@ define Package/secubox-app-peertube/install
|
||||
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse $(1)/www/cgi-bin/
|
||||
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-analyse-status $(1)/www/cgi-bin/
|
||||
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-import $(1)/www/cgi-bin/
|
||||
$(INSTALL_BIN) ./files/www/cgi-bin/peertube-import-status $(1)/www/cgi-bin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,secubox-app-peertube))
|
||||
|
||||
132
package/secubox/secubox-app-peertube/README.md
Normal file
132
package/secubox/secubox-app-peertube/README.md
Normal file
@ -0,0 +1,132 @@
|
||||
# SecuBox PeerTube
|
||||
|
||||
Federated video streaming platform running in an LXC Debian container.
|
||||
|
||||
## Features
|
||||
|
||||
- **PeerTube Instance**: Self-hosted video platform with ActivityPub federation
|
||||
- **Video Import**: Import videos from YouTube, Vimeo, and 1000+ sites via yt-dlp
|
||||
- **Multi-Track Subtitles**: Automatic subtitle download and sync in multiple languages
|
||||
- **Video Analysis**: Transcript extraction and Claude AI analysis (peertube-analyse)
|
||||
- **Live Streaming**: RTMP ingest with HLS output
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `peertubectl` | Main control script for container management |
|
||||
| `peertube-import` | Video import with subtitle sync |
|
||||
| `peertube-analyse` | Transcript extraction and AI analysis |
|
||||
|
||||
## Video Import
|
||||
|
||||
Import videos from external platforms with automatic subtitle synchronization.
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Basic import
|
||||
peertube-import https://youtube.com/watch?v=xxx
|
||||
|
||||
# Import with multiple subtitle languages
|
||||
peertube-import --lang fr,en,de,es https://youtube.com/watch?v=xxx
|
||||
|
||||
# Import as unlisted video
|
||||
peertube-import --privacy 2 https://youtube.com/watch?v=xxx
|
||||
|
||||
# Import to specific channel
|
||||
peertube-import --channel 2 https://vimeo.com/xxx
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--lang <codes>` | Subtitle languages (comma-separated) | `fr,en` |
|
||||
| `--channel <id>` | PeerTube channel ID | `1` |
|
||||
| `--privacy <level>` | 1=public, 2=unlisted, 3=private | `1` |
|
||||
| `--output <dir>` | Temp directory for downloads | `/tmp/peertube-import` |
|
||||
| `--peertube <url>` | PeerTube instance URL | from UCI config |
|
||||
|
||||
### Portal Integration
|
||||
|
||||
Access via SecuBox Portal → Intelligence & Analyse → Video Import
|
||||
|
||||
The portal provides:
|
||||
- URL input for video source
|
||||
- Language selection checkboxes
|
||||
- Privacy level selector
|
||||
- Real-time progress tracking
|
||||
- Direct link to imported video
|
||||
|
||||
### CGI Endpoints
|
||||
|
||||
```bash
|
||||
# Start import job
|
||||
curl -X POST http://192.168.255.1/cgi-bin/peertube-import \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url":"https://youtube.com/watch?v=xxx","languages":"fr,en"}'
|
||||
|
||||
# Response: {"success": true, "job_id": "import_xxx"}
|
||||
|
||||
# Check status
|
||||
curl "http://192.168.255.1/cgi-bin/peertube-import-status?job_id=import_xxx"
|
||||
|
||||
# Response (in progress):
|
||||
# {"status": "downloading", "progress": 45, "job_id": "import_xxx"}
|
||||
|
||||
# Response (completed):
|
||||
# {"success": true, "video_url": "https://tube.example.com/w/uuid"}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
UCI config file: `/etc/config/peertube`
|
||||
|
||||
```
|
||||
config peertube 'main'
|
||||
option enabled '1'
|
||||
option data_path '/srv/peertube'
|
||||
|
||||
config peertube 'server'
|
||||
option hostname 'tube.example.com'
|
||||
option port '9001'
|
||||
option https '1'
|
||||
|
||||
config peertube 'admin'
|
||||
option username 'root'
|
||||
option password 'changeme'
|
||||
|
||||
config peertube 'transcoding'
|
||||
option enabled '1'
|
||||
option threads '2'
|
||||
list resolutions '480p'
|
||||
list resolutions '720p'
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `lxc`, `lxc-common` - Container runtime
|
||||
- `wget-ssl` - HTTPS downloads
|
||||
- `tar`, `jsonfilter` - Archive and JSON handling
|
||||
- `yt-dlp` - Video download (pip install)
|
||||
- `node` - JavaScript runtime for yt-dlp (opkg install)
|
||||
|
||||
## Supported Import Sources
|
||||
|
||||
yt-dlp supports 1000+ sites including:
|
||||
- YouTube, YouTube Music
|
||||
- Vimeo
|
||||
- Dailymotion
|
||||
- Twitch (VODs)
|
||||
- Twitter/X
|
||||
- TikTok
|
||||
- And many more...
|
||||
|
||||
See: https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md
|
||||
|
||||
## Version
|
||||
|
||||
- Package: 1.2.0
|
||||
- yt-dlp: 2026.2.4 (recommended)
|
||||
- Node.js: 20.20.0 (for YouTube JS runtime)
|
||||
@ -0,0 +1,586 @@
|
||||
#!/bin/sh
|
||||
# PeerTube Video Import with Multi-Track Subtitle Sync
|
||||
# SecuBox Intelligence Module
|
||||
# Compatible: OpenWrt
|
||||
|
||||
set -e
|
||||
|
||||
#=============================================================================
|
||||
# CONFIGURATION
|
||||
#=============================================================================
|
||||
|
||||
SCRIPT_VERSION="1.0.0"
|
||||
PEERTUBE_URL="${PEERTUBE_URL:-https://tube.gk2.secubox.in}"
|
||||
PEERTUBE_TOKEN="${PEERTUBE_TOKEN:-}"
|
||||
OUTPUT_BASE="${OUTPUT_BASE:-/tmp/peertube-import}"
|
||||
DEFAULT_CHANNEL_ID=1
|
||||
DEFAULT_PRIVACY=1 # 1=public, 2=unlisted, 3=private
|
||||
|
||||
#=============================================================================
|
||||
# LOGGING
|
||||
#=============================================================================
|
||||
|
||||
log_info() { echo >&2 "[INFO] $*"; }
|
||||
log_ok() { echo >&2 "[OK] $*"; }
|
||||
log_warn() { echo >&2 "[WARN] $*"; }
|
||||
log_error() { echo >&2 "[ERROR] $*"; }
|
||||
log_step() { echo >&2 ""; echo >&2 "==> $*"; }
|
||||
log_progress() { echo >&2 "[PROGRESS] $*"; }
|
||||
|
||||
#=============================================================================
|
||||
# UTILITY FUNCTIONS
|
||||
#=============================================================================
|
||||
|
||||
# Generate slug from title
|
||||
generate_slug() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | \
|
||||
sed -E 's/[àáâãäå]/a/g; s/[èéêë]/e/g; s/[ìíîï]/i/g; s/[òóôõö]/o/g; s/[ùúûü]/u/g; s/[ç]/c/g' | \
|
||||
sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' | \
|
||||
cut -c1-50
|
||||
}
|
||||
|
||||
# Get PeerTube authentication token
|
||||
get_peertube_token() {
|
||||
local username="$1"
|
||||
local password="$2"
|
||||
|
||||
if [ -n "$PEERTUBE_TOKEN" ]; then
|
||||
echo "$PEERTUBE_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Read credentials from UCI config (admin section)
|
||||
if [ -z "$username" ]; then
|
||||
username=$(uci -q get peertube.admin.username)
|
||||
password=$(uci -q get peertube.admin.password)
|
||||
fi
|
||||
|
||||
# Try to get OAuth client credentials
|
||||
local client_id client_secret
|
||||
local oauth_clients
|
||||
oauth_clients=$(curl -s "${PEERTUBE_URL}/api/v1/oauth-clients/local")
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
client_id=$(echo "$oauth_clients" | jq -r '.client_id // empty')
|
||||
client_secret=$(echo "$oauth_clients" | jq -r '.client_secret // empty')
|
||||
else
|
||||
client_id=$(echo "$oauth_clients" | jsonfilter -e '@.client_id' 2>/dev/null)
|
||||
client_secret=$(echo "$oauth_clients" | jsonfilter -e '@.client_secret' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$client_id" ] || [ -z "$username" ]; then
|
||||
log_error "Cannot get PeerTube token. Set PEERTUBE_TOKEN or configure api credentials."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get access token
|
||||
local token_response
|
||||
token_response=$(curl -s -X POST "${PEERTUBE_URL}/api/v1/users/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "client_id=${client_id}" \
|
||||
-d "client_secret=${client_secret}" \
|
||||
-d "grant_type=password" \
|
||||
-d "response_type=code" \
|
||||
-d "username=${username}" \
|
||||
-d "password=${password}")
|
||||
|
||||
local token
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
token=$(echo "$token_response" | jq -r '.access_token // empty')
|
||||
else
|
||||
token=$(echo "$token_response" | jsonfilter -e '@.access_token' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
log_error "Failed to get access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$token"
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# VIDEO DOWNLOAD
|
||||
#=============================================================================
|
||||
|
||||
download_video() {
|
||||
local url="$1"
|
||||
local output_dir="$2"
|
||||
local slug="$3"
|
||||
|
||||
log_step "Downloading video"
|
||||
|
||||
local video_file="$output_dir/${slug}.%(ext)s"
|
||||
|
||||
# Download best quality video - redirect output to stderr to keep stdout clean
|
||||
if yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" \
|
||||
--merge-output-format mp4 \
|
||||
-o "$output_dir/${slug}.%(ext)s" \
|
||||
--no-playlist \
|
||||
"$url" >&2 2>&1; then
|
||||
|
||||
# Find downloaded file
|
||||
local found_video
|
||||
found_video=$(find "$output_dir" -name "${slug}.*" -type f | grep -E '\.(mp4|webm|mkv)$' | head -1)
|
||||
|
||||
if [ -n "$found_video" ] && [ -f "$found_video" ]; then
|
||||
log_ok "Video downloaded: $found_video"
|
||||
echo "$found_video"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log_error "Failed to download video"
|
||||
return 1
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# METADATA EXTRACTION
|
||||
#=============================================================================
|
||||
|
||||
extract_metadata() {
|
||||
local url="$1"
|
||||
local output_dir="$2"
|
||||
local slug="$3"
|
||||
|
||||
log_step "Extracting metadata"
|
||||
|
||||
local meta_file="$output_dir/${slug}.meta.json"
|
||||
|
||||
if yt-dlp --dump-json --no-warnings "$url" 2>/dev/null > "$meta_file.tmp"; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq '{
|
||||
id: .id,
|
||||
title: .title,
|
||||
description: .description,
|
||||
duration: .duration,
|
||||
upload_date: .upload_date,
|
||||
uploader: .uploader,
|
||||
channel: .channel,
|
||||
tags: .tags,
|
||||
webpage_url: .webpage_url,
|
||||
thumbnail: .thumbnail,
|
||||
subtitles: ((.subtitles // {}) | keys),
|
||||
automatic_captions: ((.automatic_captions // {}) | keys)
|
||||
}' "$meta_file.tmp" > "$meta_file"
|
||||
else
|
||||
mv "$meta_file.tmp" "$meta_file"
|
||||
fi
|
||||
rm -f "$meta_file.tmp"
|
||||
log_ok "Metadata saved: $meta_file"
|
||||
echo "$meta_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Failed to extract metadata"
|
||||
return 1
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# SUBTITLE DOWNLOAD
|
||||
#=============================================================================
|
||||
|
||||
download_subtitles() {
|
||||
local url="$1"
|
||||
local output_dir="$2"
|
||||
local slug="$3"
|
||||
local languages="$4" # Comma-separated: fr,en,de
|
||||
|
||||
log_step "Downloading subtitles"
|
||||
|
||||
[ -z "$languages" ] && languages="fr,en"
|
||||
|
||||
log_info "Requested languages: $languages"
|
||||
|
||||
# Download subtitles with yt-dlp
|
||||
yt-dlp --write-sub --write-auto-sub \
|
||||
--sub-lang "$languages" \
|
||||
--sub-format vtt \
|
||||
--convert-subs vtt \
|
||||
--skip-download \
|
||||
-o "$output_dir/${slug}" \
|
||||
"$url" 2>&1 || true
|
||||
|
||||
# List downloaded subtitle files
|
||||
local count=0
|
||||
for vtt in "$output_dir"/${slug}*.vtt; do
|
||||
[ -f "$vtt" ] || continue
|
||||
count=$((count + 1))
|
||||
log_ok "Downloaded: $(basename "$vtt")"
|
||||
done
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
log_warn "No subtitles found for requested languages"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_ok "Downloaded $count subtitle file(s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# PEERTUBE UPLOAD
|
||||
#=============================================================================
|
||||
|
||||
upload_video_to_peertube() {
|
||||
local video_file="$1"
|
||||
local meta_file="$2"
|
||||
local channel_id="$3"
|
||||
local privacy="$4"
|
||||
local token="$5"
|
||||
|
||||
log_step "Uploading video to PeerTube"
|
||||
|
||||
[ -z "$channel_id" ] && channel_id=$DEFAULT_CHANNEL_ID
|
||||
[ -z "$privacy" ] && privacy=$DEFAULT_PRIVACY
|
||||
|
||||
# Get title and description from metadata
|
||||
local title description
|
||||
if command -v jq >/dev/null 2>&1 && [ -f "$meta_file" ]; then
|
||||
title=$(jq -r '.title // "Imported Video"' "$meta_file")
|
||||
description=$(jq -r '.description // ""' "$meta_file" | head -c 10000)
|
||||
else
|
||||
title="Imported Video"
|
||||
description=""
|
||||
fi
|
||||
|
||||
log_info "Title: $title"
|
||||
log_info "Channel ID: $channel_id"
|
||||
log_info "Privacy: $privacy"
|
||||
|
||||
# Sanitize description (escape newlines and quotes)
|
||||
description=$(echo "$description" | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 5000)
|
||||
|
||||
# Upload video - use temp file to avoid mixing stderr with response
|
||||
local tmpfile="/tmp/peertube_upload_$$.json"
|
||||
local http_code
|
||||
|
||||
http_code=$(curl -s -w "%{http_code}" -o "$tmpfile" -X POST "${PEERTUBE_URL}/api/v1/videos/upload" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-F "videofile=@${video_file}" \
|
||||
-F "channelId=${channel_id}" \
|
||||
-F "name=${title}" \
|
||||
-F "privacy=${privacy}" \
|
||||
-F "description=${description}" \
|
||||
2>/dev/null)
|
||||
|
||||
local body=""
|
||||
[ -f "$tmpfile" ] && body=$(cat "$tmpfile")
|
||||
rm -f "$tmpfile"
|
||||
|
||||
if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
|
||||
local video_id video_uuid
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
video_id=$(echo "$body" | jq -r '.video.id // empty')
|
||||
video_uuid=$(echo "$body" | jq -r '.video.uuid // empty')
|
||||
else
|
||||
video_id=$(echo "$body" | jsonfilter -e '@.video.id' 2>/dev/null)
|
||||
video_uuid=$(echo "$body" | jsonfilter -e '@.video.uuid' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -n "$video_id" ]; then
|
||||
log_ok "Video uploaded: ID=$video_id, UUID=$video_uuid"
|
||||
echo "${video_id}|${video_uuid}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log_error "Upload failed (HTTP $http_code)"
|
||||
log_error "$body"
|
||||
return 1
|
||||
}
|
||||
|
||||
upload_subtitles_to_peertube() {
|
||||
local video_id="$1"
|
||||
local output_dir="$2"
|
||||
local slug="$3"
|
||||
local token="$4"
|
||||
|
||||
log_step "Uploading subtitles to PeerTube"
|
||||
|
||||
local uploaded=0
|
||||
|
||||
for vtt in "$output_dir"/${slug}*.vtt; do
|
||||
[ -f "$vtt" ] || continue
|
||||
|
||||
# Extract language code from filename
|
||||
# Format: slug.fr.vtt or slug.en-US.vtt
|
||||
local filename
|
||||
filename=$(basename "$vtt")
|
||||
local lang
|
||||
lang=$(echo "$filename" | sed -E "s/${slug}\.([a-zA-Z-]+)\.vtt/\1/")
|
||||
|
||||
# Normalize language code (en-US -> en)
|
||||
lang=$(echo "$lang" | cut -d'-' -f1)
|
||||
|
||||
log_info "Uploading subtitle: $lang ($filename)"
|
||||
|
||||
local http_code
|
||||
http_code=$(curl -s -w "%{http_code}" -o /dev/null -X PUT \
|
||||
"${PEERTUBE_URL}/api/v1/videos/${video_id}/captions/${lang}" \
|
||||
-H "Authorization: Bearer ${token}" \
|
||||
-F "captionfile=@${vtt}" \
|
||||
2>/dev/null)
|
||||
|
||||
if [ "$http_code" = "200" ] || [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then
|
||||
log_ok "Uploaded: $lang"
|
||||
uploaded=$((uploaded + 1))
|
||||
else
|
||||
log_warn "Failed to upload $lang (HTTP $http_code)"
|
||||
fi
|
||||
done
|
||||
|
||||
log_ok "Uploaded $uploaded subtitle(s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# MAIN IMPORT FUNCTION
|
||||
#=============================================================================
|
||||
|
||||
import_video() {
|
||||
local url="$1"
|
||||
local languages="$2"
|
||||
local channel_id="$3"
|
||||
local privacy="$4"
|
||||
local username="$5"
|
||||
local password="$6"
|
||||
|
||||
log_step "Starting video import"
|
||||
log_info "URL: $url"
|
||||
|
||||
# Create output directory
|
||||
# If OUTPUT_BASE already contains import_ (called from CGI), use it as-is
|
||||
local output_dir
|
||||
if echo "$OUTPUT_BASE" | grep -q "/import_"; then
|
||||
output_dir="$OUTPUT_BASE"
|
||||
else
|
||||
output_dir="$OUTPUT_BASE/import_$(date +%s)_$$"
|
||||
fi
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
# Extract metadata first to get title
|
||||
local meta_file
|
||||
meta_file=$(extract_metadata "$url" "$output_dir" "video") || {
|
||||
log_error "Metadata extraction failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Generate slug from title
|
||||
local title slug
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
title=$(jq -r '.title // "video"' "$meta_file")
|
||||
else
|
||||
title=$(jsonfilter -i "$meta_file" -e '@.title' 2>/dev/null || echo "video")
|
||||
fi
|
||||
slug=$(generate_slug "$title")
|
||||
[ -z "$slug" ] && slug="video_$$"
|
||||
|
||||
# Rename metadata file
|
||||
mv "$meta_file" "$output_dir/${slug}.meta.json"
|
||||
meta_file="$output_dir/${slug}.meta.json"
|
||||
|
||||
log_progress "downloading"
|
||||
|
||||
# Download video
|
||||
local video_file
|
||||
video_file=$(download_video "$url" "$output_dir" "$slug") || {
|
||||
log_error "Video download failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Download subtitles
|
||||
download_subtitles "$url" "$output_dir" "$slug" "$languages" || {
|
||||
log_warn "Subtitle download failed (continuing without subtitles)"
|
||||
}
|
||||
|
||||
log_progress "uploading"
|
||||
|
||||
# Get PeerTube token
|
||||
local token
|
||||
token=$(get_peertube_token "$username" "$password") || {
|
||||
log_error "Authentication failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Upload video
|
||||
local upload_result
|
||||
upload_result=$(upload_video_to_peertube "$video_file" "$meta_file" "$channel_id" "$privacy" "$token") || {
|
||||
log_error "Video upload failed"
|
||||
return 1
|
||||
}
|
||||
|
||||
local video_id video_uuid
|
||||
video_id=$(echo "$upload_result" | cut -d'|' -f1)
|
||||
video_uuid=$(echo "$upload_result" | cut -d'|' -f2)
|
||||
|
||||
# Upload subtitles
|
||||
upload_subtitles_to_peertube "$video_id" "$output_dir" "$slug" "$token" || {
|
||||
log_warn "Subtitle upload failed"
|
||||
}
|
||||
|
||||
log_progress "completed"
|
||||
|
||||
# Output result
|
||||
log_step "Import complete"
|
||||
log_ok "Video ID: $video_id"
|
||||
log_ok "Video UUID: $video_uuid"
|
||||
log_ok "URL: ${PEERTUBE_URL}/w/${video_uuid}"
|
||||
|
||||
# Output JSON result
|
||||
cat << EOF
|
||||
{
|
||||
"success": true,
|
||||
"video_id": $video_id,
|
||||
"video_uuid": "$video_uuid",
|
||||
"video_url": "${PEERTUBE_URL}/w/${video_uuid}",
|
||||
"title": $(echo "$title" | jq -Rs . 2>/dev/null || echo "\"$title\""),
|
||||
"output_dir": "$output_dir"
|
||||
}
|
||||
EOF
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# CLI PARSING
|
||||
#=============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
PeerTube Video Import with Subtitle Sync
|
||||
SecuBox Intelligence Module v${SCRIPT_VERSION}
|
||||
|
||||
Usage:
|
||||
$(basename "$0") [OPTIONS] <url>
|
||||
|
||||
Options:
|
||||
--url <url> Video URL (YouTube, Vimeo, etc.)
|
||||
--lang <codes> Subtitle languages (comma-separated: fr,en,de)
|
||||
Default: fr,en
|
||||
--channel <id> PeerTube channel ID (default: 1)
|
||||
--privacy <level> 1=public, 2=unlisted, 3=private (default: 1)
|
||||
--username <user> PeerTube username (or set via UCI)
|
||||
--password <pass> PeerTube password (or set via UCI)
|
||||
--output <dir> Output directory (default: /tmp/peertube-import)
|
||||
--peertube <url> PeerTube instance URL
|
||||
-h, --help Show this help message
|
||||
|
||||
Environment Variables:
|
||||
PEERTUBE_URL PeerTube instance URL
|
||||
PEERTUBE_TOKEN OAuth access token (skip authentication)
|
||||
|
||||
Examples:
|
||||
# Basic import
|
||||
$(basename "$0") https://youtube.com/watch?v=xxx
|
||||
|
||||
# Import with multiple subtitle languages
|
||||
$(basename "$0") --lang fr,en,de,es https://youtube.com/watch?v=xxx
|
||||
|
||||
# Import as unlisted video
|
||||
$(basename "$0") --privacy 2 https://youtube.com/watch?v=xxx
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
VIDEO_URL=""
|
||||
LANGUAGES="fr,en"
|
||||
CHANNEL_ID=""
|
||||
PRIVACY=""
|
||||
USERNAME=""
|
||||
PASSWORD=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--url)
|
||||
VIDEO_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--lang|--languages)
|
||||
LANGUAGES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--channel)
|
||||
CHANNEL_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--privacy)
|
||||
PRIVACY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--username)
|
||||
USERNAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--password)
|
||||
PASSWORD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_BASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--peertube)
|
||||
PEERTUBE_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
log_error "Unknown option: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
# Positional argument = URL
|
||||
if [ -z "$VIDEO_URL" ]; then
|
||||
VIDEO_URL="$1"
|
||||
else
|
||||
log_error "Multiple URLs not supported"
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$VIDEO_URL" ]; then
|
||||
log_error "No video URL provided"
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#=============================================================================
|
||||
# ENTRY POINT
|
||||
#=============================================================================
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
echo >&2 ""
|
||||
echo >&2 "╔══════════════════════════════════════════════════════╗"
|
||||
echo >&2 "║ PeerTube Video Import v${SCRIPT_VERSION} ║"
|
||||
echo >&2 "║ SecuBox Intelligence Module ║"
|
||||
echo >&2 "╚══════════════════════════════════════════════════════╝"
|
||||
echo >&2 ""
|
||||
|
||||
# Check dependencies
|
||||
for dep in yt-dlp curl; do
|
||||
if ! command -v "$dep" >/dev/null 2>&1; then
|
||||
log_error "Required dependency not found: $dep"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
import_video "$VIDEO_URL" "$LANGUAGES" "$CHANNEL_ID" "$PRIVACY" "$USERNAME" "$PASSWORD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
# Run if not sourced
|
||||
if [ "${0##*/}" = "peertube-import" ] || [ "${0##*/}" = "sh" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
@ -0,0 +1,164 @@
|
||||
#!/bin/sh
|
||||
# CGI endpoint for PeerTube video import
|
||||
# Returns JSON response
|
||||
|
||||
# Set headers
|
||||
printf "Content-Type: application/json\r\n"
|
||||
printf "Access-Control-Allow-Origin: *\r\n"
|
||||
printf "Access-Control-Allow-Methods: POST, OPTIONS\r\n"
|
||||
printf "Access-Control-Allow-Headers: Content-Type\r\n"
|
||||
printf "\r\n"
|
||||
|
||||
# Handle OPTIONS (CORS preflight)
|
||||
if [ "$REQUEST_METHOD" = "OPTIONS" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only allow POST
|
||||
if [ "$REQUEST_METHOD" != "POST" ]; then
|
||||
echo '{"error": "Method not allowed"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Read input
|
||||
INPUT=$(cat)
|
||||
|
||||
# Parse JSON (use jq if available, else jsonfilter)
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
URL=$(echo "$INPUT" | jq -r '.url // empty')
|
||||
LANGUAGES=$(echo "$INPUT" | jq -r '.languages // "fr,en"')
|
||||
CHANNEL_ID=$(echo "$INPUT" | jq -r '.channel_id // "1"')
|
||||
PRIVACY=$(echo "$INPUT" | jq -r '.privacy // "1"')
|
||||
else
|
||||
URL=$(echo "$INPUT" | jsonfilter -e '@.url' 2>/dev/null)
|
||||
LANGUAGES=$(echo "$INPUT" | jsonfilter -e '@.languages' 2>/dev/null)
|
||||
CHANNEL_ID=$(echo "$INPUT" | jsonfilter -e '@.channel_id' 2>/dev/null)
|
||||
PRIVACY=$(echo "$INPUT" | jsonfilter -e '@.privacy' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Handle array of languages
|
||||
if echo "$LANGUAGES" | grep -q '^\['; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
LANGUAGES=$(echo "$INPUT" | jq -r '.languages | if type == "array" then join(",") else . end')
|
||||
else
|
||||
LANGUAGES="fr,en"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate URL
|
||||
if [ -z "$URL" ]; then
|
||||
echo '{"error": "URL is required"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Sanitize URL (basic security check)
|
||||
case "$URL" in
|
||||
http://*|https://*)
|
||||
# Valid URL prefix
|
||||
;;
|
||||
*)
|
||||
echo '{"error": "Invalid URL format"}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Set defaults
|
||||
[ -z "$LANGUAGES" ] && LANGUAGES="fr,en"
|
||||
[ -z "$CHANNEL_ID" ] && CHANNEL_ID="1"
|
||||
[ -z "$PRIVACY" ] && PRIVACY="1"
|
||||
|
||||
# Generate job ID
|
||||
JOB_ID="import_$(date +%s)_$$"
|
||||
STATUS_FILE="/tmp/peertube-import-${JOB_ID}.status"
|
||||
RESULT_FILE="/tmp/peertube-import-${JOB_ID}.json"
|
||||
LOG_FILE="/tmp/peertube-import-${JOB_ID}.log"
|
||||
PROGRESS_FILE="/tmp/peertube-import-${JOB_ID}.progress"
|
||||
|
||||
# Check for import script
|
||||
if [ ! -x "/usr/sbin/peertube-import" ]; then
|
||||
echo '{"error": "peertube-import not installed"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize status
|
||||
echo "starting" > "$STATUS_FILE"
|
||||
echo "0" > "$PROGRESS_FILE"
|
||||
|
||||
# Start import in background
|
||||
(
|
||||
echo "downloading" > "$STATUS_FILE"
|
||||
echo "10" > "$PROGRESS_FILE"
|
||||
|
||||
# Run the import
|
||||
OUTPUT=$(/usr/sbin/peertube-import \
|
||||
--lang "$LANGUAGES" \
|
||||
--channel "$CHANNEL_ID" \
|
||||
--privacy "$PRIVACY" \
|
||||
"$URL" 2>&1)
|
||||
RC=$?
|
||||
|
||||
echo "$OUTPUT" >> "$LOG_FILE"
|
||||
|
||||
if [ $RC -eq 0 ]; then
|
||||
echo "completed" > "$STATUS_FILE"
|
||||
echo "100" > "$PROGRESS_FILE"
|
||||
|
||||
# Extract JSON result from output (last JSON block)
|
||||
local result_json
|
||||
result_json=$(echo "$OUTPUT" | grep -E '^\{' | tail -1)
|
||||
|
||||
if [ -n "$result_json" ]; then
|
||||
# Parse and rebuild result
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
VIDEO_ID=$(echo "$result_json" | jq -r '.video_id // empty')
|
||||
VIDEO_UUID=$(echo "$result_json" | jq -r '.video_uuid // empty')
|
||||
VIDEO_URL=$(echo "$result_json" | jq -r '.video_url // empty')
|
||||
TITLE=$(echo "$result_json" | jq -r '.title // "Imported Video"')
|
||||
else
|
||||
VIDEO_ID=$(echo "$result_json" | jsonfilter -e '@.video_id' 2>/dev/null)
|
||||
VIDEO_UUID=$(echo "$result_json" | jsonfilter -e '@.video_uuid' 2>/dev/null)
|
||||
VIDEO_URL=$(echo "$result_json" | jsonfilter -e '@.video_url' 2>/dev/null)
|
||||
TITLE="Imported Video"
|
||||
fi
|
||||
|
||||
cat > "$RESULT_FILE" << EOF
|
||||
{
|
||||
"success": true,
|
||||
"job_id": "$JOB_ID",
|
||||
"video_id": $VIDEO_ID,
|
||||
"video_uuid": "$VIDEO_UUID",
|
||||
"video_url": "$VIDEO_URL",
|
||||
"title": "$TITLE",
|
||||
"status": "completed"
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > "$RESULT_FILE" << EOF
|
||||
{
|
||||
"success": true,
|
||||
"job_id": "$JOB_ID",
|
||||
"status": "completed",
|
||||
"message": "Import completed but no result JSON found"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
else
|
||||
echo "failed" > "$STATUS_FILE"
|
||||
echo "0" > "$PROGRESS_FILE"
|
||||
|
||||
# Get last error from log
|
||||
ERROR_MSG=$(tail -5 "$LOG_FILE" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 500)
|
||||
|
||||
cat > "$RESULT_FILE" << EOF
|
||||
{
|
||||
"success": false,
|
||||
"job_id": "$JOB_ID",
|
||||
"status": "failed",
|
||||
"error": "$ERROR_MSG"
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
) &
|
||||
|
||||
# Return job ID for polling
|
||||
echo "{\"success\": true, \"message\": \"Import started\", \"job_id\": \"$JOB_ID\"}"
|
||||
@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# CGI endpoint for checking PeerTube import status
|
||||
# Returns JSON response
|
||||
|
||||
# Set headers
|
||||
printf "Content-Type: application/json\r\n"
|
||||
printf "Access-Control-Allow-Origin: *\r\n"
|
||||
printf "\r\n"
|
||||
|
||||
# Get job_id from query string or POST body
|
||||
JOB_ID=""
|
||||
|
||||
if [ -n "$QUERY_STRING" ]; then
|
||||
JOB_ID=$(echo "$QUERY_STRING" | sed -n 's/.*job_id=\([^&]*\).*/\1/p')
|
||||
fi
|
||||
|
||||
if [ -z "$JOB_ID" ] && [ "$REQUEST_METHOD" = "POST" ]; then
|
||||
INPUT=$(cat)
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
JOB_ID=$(echo "$INPUT" | jq -r '.job_id // empty')
|
||||
else
|
||||
JOB_ID=$(echo "$INPUT" | jsonfilter -e '@.job_id' 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
echo '{"error": "job_id is required"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Sanitize job_id (only allow alphanumeric and underscore)
|
||||
JOB_ID=$(echo "$JOB_ID" | tr -cd 'a-zA-Z0-9_')
|
||||
|
||||
STATUS_FILE="/tmp/peertube-import-${JOB_ID}.status"
|
||||
RESULT_FILE="/tmp/peertube-import-${JOB_ID}.json"
|
||||
LOG_FILE="/tmp/peertube-import-${JOB_ID}.log"
|
||||
PROGRESS_FILE="/tmp/peertube-import-${JOB_ID}.progress"
|
||||
|
||||
# Check if job exists
|
||||
if [ ! -f "$STATUS_FILE" ]; then
|
||||
echo '{"error": "Job not found", "job_id": "'"$JOB_ID"'"}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STATUS=$(cat "$STATUS_FILE" 2>/dev/null || echo "unknown")
|
||||
PROGRESS=$(cat "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
# If completed or failed, return full result
|
||||
if [ -f "$RESULT_FILE" ]; then
|
||||
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
|
||||
cat "$RESULT_FILE"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Otherwise return status with progress and logs
|
||||
LOGS=""
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
LOGS=$(tail -3 "$LOG_FILE" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 300)
|
||||
fi
|
||||
|
||||
cat << EOF
|
||||
{
|
||||
"status": "$STATUS",
|
||||
"job_id": "$JOB_ID",
|
||||
"progress": $PROGRESS,
|
||||
"logs": "$LOGS"
|
||||
}
|
||||
EOF
|
||||
Loading…
Reference in New Issue
Block a user