452 lines
14 KiB
Markdown
452 lines
14 KiB
Markdown
# movmaker-webui
|
|
|
|
A small PHP web interface for publishing and managing **MVLog** videos on Bubulescu.Org.
|
|
|
|
The app has two sides:
|
|
|
|
- **Public landing page**: lists rendered videos from `out-dir/`.
|
|
- **Admin page**: creates and edits movmaker input directories in `in-dir/`.
|
|
|
|
Generated videos are rendered by the separate `movmaker` worker script and published back into this web app.
|
|
|
|
## Paths and URLs
|
|
|
|
Production location:
|
|
|
|
```text
|
|
/var/www/html/mvlog
|
|
```
|
|
|
|
Public URLs:
|
|
|
|
```text
|
|
https://bubulescu.org/mvlog/
|
|
https://bubulescu.org/mvlog/new.php
|
|
https://bubulescu.org/mvlog/login.php
|
|
https://bubulescu.org/mvlog/feed.php
|
|
```
|
|
|
|
Important directories:
|
|
|
|
```text
|
|
in-dir/ Admin-created movmaker input directories
|
|
out-dir/ Rendered public videos
|
|
cache/ Cached video metadata
|
|
assets/ CSS assets, background image, fonts
|
|
thumbs/ Reserved for thumbnails
|
|
```
|
|
|
|
## Main files
|
|
|
|
```text
|
|
index.php Public video landing page
|
|
new.php Protected admin interface
|
|
login.php PHP session login page
|
|
logout.php Destroys admin session and redirects to login
|
|
feed.php RSS feed of latest videos
|
|
auth.php PHP session authentication helpers
|
|
config.php Main app config
|
|
style.css Site/admin styling
|
|
```
|
|
|
|
## Features
|
|
|
|
### Public landing page
|
|
|
|
`index.php` lists videos from `out-dir/`.
|
|
|
|
Features:
|
|
|
|
- Dark cinematic MVLog theme
|
|
- Sticky compact header
|
|
- RSS icon in the header
|
|
- One video per row
|
|
- Metadata display:
|
|
- title
|
|
- event date from embedded metadata
|
|
- location when available
|
|
- description when available
|
|
- video creation/publish timestamp in the top-right corner of each row
|
|
- Download discouraged with:
|
|
- `controlsList="nodownload"`
|
|
- right-click disabled on videos
|
|
- Video metadata cached in `cache/videos.json`
|
|
|
|
### RSS feed
|
|
|
|
`feed.php` exposes an RSS feed of recent videos:
|
|
|
|
```text
|
|
https://bubulescu.org/mvlog/feed.php
|
|
```
|
|
|
|
The feed includes video title, link, description, publication date, and video enclosure.
|
|
|
|
### New badges
|
|
|
|
Recent videos can be styled with a `New` badge. The CSS class is:
|
|
|
|
```css
|
|
.new-badge
|
|
```
|
|
|
|
### Admin page
|
|
|
|
`new.php` is protected by PHP session login.
|
|
|
|
Admin tabs:
|
|
|
|
- **New**: create a new input directory
|
|
- **Edit**: edit existing input directories
|
|
- **Videos**: manage orphan videos not referenced by `.movmaker-state.json`
|
|
|
|
Admin can:
|
|
|
|
- Create input dirs
|
|
- Upload images/videos/audio
|
|
- Edit metadata
|
|
- Edit per-file captions for images/videos
|
|
- Remove media files
|
|
- Delete input dirs
|
|
- Optionally delete the generated output video associated with an input dir
|
|
- Delete orphan videos
|
|
|
|
Internal files such as `.movmaker-state.json` and `data.txt` are not shown as removable media files.
|
|
|
|
## Authentication
|
|
|
|
The admin area uses PHP sessions, not nginx Basic Auth.
|
|
|
|
Auth files:
|
|
|
|
```text
|
|
/var/www/html/mvlog/auth.php
|
|
/etc/mvlog/auth.php
|
|
```
|
|
|
|
`/etc/mvlog/auth.php` stores username/password hashes outside the web root:
|
|
|
|
```php
|
|
<?php
|
|
return [
|
|
'users' => [
|
|
'username' => 'password_hash_here',
|
|
],
|
|
];
|
|
```
|
|
|
|
The password hash should be generated with PHP `password_hash()`.
|
|
|
|
Example:
|
|
|
|
```bash
|
|
php -r 'echo password_hash("new-password", PASSWORD_DEFAULT), PHP_EOL;'
|
|
```
|
|
|
|
Then update `/etc/mvlog/auth.php`.
|
|
|
|
Logout:
|
|
|
|
```text
|
|
/mvlog/logout.php
|
|
```
|
|
|
|
Logout destroys the PHP session and redirects to `login.php`.
|
|
|
|
## Input directory naming
|
|
|
|
New admin-created input directories must use:
|
|
|
|
```text
|
|
YYYYMMDD_slug-title
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
20260525_mohnesee
|
|
```
|
|
|
|
Do **not** include time in the directory name.
|
|
|
|
## `data.txt` format
|
|
|
|
Each input directory contains a `data.txt` file.
|
|
|
|
All entries use mandatory `key: value` syntax:
|
|
|
|
```text
|
|
Title: My Movie Title
|
|
Location: Sønderborg
|
|
Date: 2026-05-25
|
|
Description: Short description of the video.
|
|
filename.jpg: Optional caption for this file
|
|
filename.mp4: Optional caption for this clip
|
|
```
|
|
|
|
Supported general keys:
|
|
|
|
```text
|
|
Title
|
|
Location
|
|
Date
|
|
Description
|
|
```
|
|
|
|
Per-file captions are stored as:
|
|
|
|
```text
|
|
filename.ext: caption text
|
|
```
|
|
|
|
Audio files do not get captions in the admin UI.
|
|
|
|
## Worker integration
|
|
|
|
Rendering is handled by the separate `movmaker` project, especially:
|
|
|
|
```text
|
|
/home/hbrain/source/movmaker/mvlog_worker.py
|
|
```
|
|
|
|
The worker:
|
|
|
|
1. Scans remote `in-dir/` directories.
|
|
2. Computes an input fingerprint.
|
|
3. Compares with `.movmaker-state.json`.
|
|
4. Renders changed jobs with `movmaker.py`.
|
|
5. Publishes MP4 files to `out-dir/`.
|
|
6. Updates `.movmaker-state.json`.
|
|
7. Clears `cache/videos.json` after publishing.
|
|
|
|
State file per input dir:
|
|
|
|
```text
|
|
in-dir/<job>/.movmaker-state.json
|
|
```
|
|
|
|
The state file maps an input dir to its generated output video. Orphan detection must rely on this state file only.
|
|
|
|
Systemd units on the worker machine:
|
|
|
|
```text
|
|
/etc/systemd/system/mvlog-worker.service
|
|
/etc/systemd/system/mvlog-worker.timer
|
|
```
|
|
|
|
Useful commands:
|
|
|
|
```bash
|
|
systemctl status mvlog-worker.service --no-pager
|
|
systemctl status mvlog-worker.timer --no-pager
|
|
journalctl -u mvlog-worker.service -n 100 --no-pager
|
|
```
|
|
|
|
## Orphan video handling
|
|
|
|
The admin **Videos** tab shows generated videos in `out-dir/` that are not referenced by any input directory state file.
|
|
|
|
Important rule:
|
|
|
|
> Orphan detection must use `.movmaker-state.json` only. Do not infer ownership from metadata or title fallback.
|
|
|
|
## Configuration
|
|
|
|
`config.php` returns the main app config:
|
|
|
|
```php
|
|
return [
|
|
"site_name" => "Movmaker WebUI",
|
|
"videos_dir" => __DIR__ . "/out-dir",
|
|
"thumbs_dir" => __DIR__ . "/thumbs",
|
|
"uploads_dir" => __DIR__ . "/in-dir",
|
|
"public_videos" => "out-dir",
|
|
"public_thumbs" => "thumbs",
|
|
"items_per_page" => 12,
|
|
];
|
|
```
|
|
|
|
## nginx/PHP requirements
|
|
|
|
The app runs under nginx + PHP-FPM.
|
|
|
|
Admin uploads require larger request limits. Production currently uses:
|
|
|
|
```nginx
|
|
client_max_body_size 512M;
|
|
```
|
|
|
|
PHP-FPM upload override:
|
|
|
|
```ini
|
|
upload_max_filesize = 512M
|
|
post_max_size = 512M
|
|
max_file_uploads = 100
|
|
max_input_time = 600
|
|
max_execution_time = 600
|
|
```
|
|
|
|
Example file:
|
|
|
|
```text
|
|
/etc/php/8.2/fpm/conf.d/99-mvlog-upload.ini
|
|
```
|
|
|
|
After changing nginx/PHP config:
|
|
|
|
```bash
|
|
sudo nginx -t
|
|
sudo systemctl reload nginx
|
|
sudo systemctl restart php8.2-fpm
|
|
```
|
|
|
|
## Timezone
|
|
|
|
The public page and RSS feed explicitly use:
|
|
|
|
```php
|
|
date_default_timezone_set('Europe/Copenhagen');
|
|
```
|
|
|
|
This ensures displayed creation/publish times are Danish local time, not UTC.
|
|
|
|
## Permissions
|
|
|
|
Expected web ownership is generally:
|
|
|
|
```text
|
|
www-data:www-data
|
|
```
|
|
|
|
The git repo may be owned by `hbrain:www-data`.
|
|
|
|
State files should be group-writable so the worker can update them:
|
|
|
|
```bash
|
|
chmod 664 in-dir/*/.movmaker-state.json
|
|
```
|
|
|
|
The worker writes state files using a temp file and `chmod 664` to avoid permission problems.
|
|
|
|
## Git notes
|
|
|
|
Repository:
|
|
|
|
```text
|
|
https://git.novosel.dk/marijo/movmaker-webui.git
|
|
```
|
|
|
|
If git complains about ownership:
|
|
|
|
```bash
|
|
git config --global --add safe.directory /var/www/html/mvlog
|
|
sudo chown -R hbrain:www-data /var/www/html/mvlog/.git
|
|
```
|
|
|
|
Do not commit generated media from `in-dir/` or `out-dir/`.
|
|
|
|
`AGENTS.md` is local assistant/project context and should remain ignored by git.
|
|
|
|
## Server-side web-push notifications
|
|
|
|
The MVLog web UI includes a small server-side push integration so the admin can immediately notify subscribers when a job is made "Show". Keys and sending logic live on the webserver to keep VAPID private keys secret and to avoid exposing them to worker nodes.
|
|
|
|
Key files and behavior (deployed under /var/www/html/mvlog):
|
|
|
|
- lib/send_push.php — PHP helper that invokes the Node sender and atomically records send state in cache/push_notifications.json.
|
|
- send_push.js — Node script (uses the package) to send individual push messages; it reads subscriptions and push config and performs sends.
|
|
- cache/push_subscriptions.json — stored subscriber list (array of subscription objects).
|
|
- cache/push_notifications.json — recorded sends (shown map) to avoid duplicate notifications.
|
|
- api/enable_show.php — authenticated endpoint to toggle Show and trigger a server-side send (requires admin login).
|
|
- new.php — admin UI now triggers the server-side helper when Show is enabled.
|
|
- /etc/mvlog/push.php or /var/www/html/mvlog/push.json — VAPID key holder (private key must remain secret; set owner to root or www-data and permissions to 600/640).
|
|
- log: /var/log/mvlog_notify.log — send attempts and errors are logged here.
|
|
|
|
Installation notes:
|
|
|
|
1. Install Node and web-push in the webroot:
|
|
|
|
npm install web-push --prefix /var/www/html/mvlog
|
|
|
|
2. Place VAPID keys where the webserver can read them (recommended: /etc/mvlog/push.php) and set restrictive permissions (600 or 640, owner root:www-data). Do NOT commit private keys to git.
|
|
|
|
3. Ensure /var/www/html/mvlog/cache exists and contains push_subscriptions.json and push_notifications.json (create {} if empty), owned by www-data and writable.
|
|
|
|
Security notes:
|
|
|
|
- api/enable_show.php requires admin login; do not expose it publicly without strong auth.
|
|
- Consider adding CSRF protection, rate limiting, and periodic pruning of expired subscriptions (HTTP 410 responses) to reduce send failures.
|
|
|
|
|
|
## Gemini-based description generator
|
|
|
|
A helper script on the webserver can analyze an input directory (images/videos) and produce a short JSON description using the Google Generative Language (Gemini) API.
|
|
|
|
- Script: /var/www/html/mvlog/bin/generate_data.sh
|
|
- Purpose: collect representative frames, call Gemini, save the raw response as <inputdir>/gemini_generated.json, and append a Description block to <inputdir>/data.txt only if no Description already exists.
|
|
|
|
What it does:
|
|
|
|
- Collects up to N frames from the input dir (images -> 1 frame, videos -> up to 3 frames depending on duration).
|
|
- Builds a JSON request containing the prompt and inline base64-encoded images using a Python helper to avoid ARG_MAX issues.
|
|
- Calls the Gemini API and saves the returned JSON text to <inputdir>/gemini_generated.json for review.
|
|
- Appends only a Description block (Description:
|
|
## Server-side web-push notifications
|
|
|
|
The MVLog web UI includes a small server-side push integration so the admin can immediately notify subscribers when a job is made "Show". Keys and sending logic live on the webserver to keep VAPID private keys secret and to avoid exposing them to worker nodes.
|
|
|
|
Key files and behavior (deployed under /var/www/html/mvlog):
|
|
|
|
- lib/send_push.php — PHP helper that invokes the Node sender and atomically records send state in cache/push_notifications.json.
|
|
- send_push.js — Node script (uses the `web-push` package) to send individual push messages; it reads subscriptions and push config and performs sends.
|
|
- cache/push_subscriptions.json — stored subscriber list (array of subscription objects).
|
|
- cache/push_notifications.json — recorded sends ("shown" map) to avoid duplicate notifications.
|
|
- api/enable_show.php — authenticated endpoint to toggle Show and trigger a server-side send (requires admin login).
|
|
- new.php — admin UI now triggers the server-side helper when Show is enabled.
|
|
- /etc/mvlog/push.php or /var/www/html/mvlog/push.json — VAPID key holder (private key must remain secret; set owner to root or www-data and permissions to 600/640).
|
|
- log: /var/log/mvlog_notify.log — send attempts and errors are logged here.
|
|
|
|
Installation notes:
|
|
|
|
1. Install Node and web-push in the webroot:
|
|
|
|
npm install web-push --prefix /var/www/html/mvlog
|
|
|
|
2. Place VAPID keys where the webserver can read them (recommended: /etc/mvlog/push.php) and set restrictive permissions (600 or 640, owner root:www-data). Do NOT commit private keys to git.
|
|
|
|
3. Ensure /var/www/html/mvlog/cache exists and contains push_subscriptions.json and push_notifications.json (create {} if empty), owned by www-data and writable.
|
|
|
|
Security notes:
|
|
|
|
- api/enable_show.php requires admin login; do not expose it publicly without strong auth.
|
|
- Consider adding CSRF protection, rate limiting, and periodic pruning of expired subscriptions (HTTP 410 responses) to reduce send failures.
|
|
|
|
|
|
## Gemini-based description generator
|
|
|
|
A helper script on the webserver can analyze an input directory (images/videos) and produce a short JSON description using the Google Generative Language (Gemini) API.
|
|
|
|
- Script: /var/www/html/mvlog/bin/generate_data.sh
|
|
- Purpose: collect representative frames, call Gemini, save the raw response as <inputdir>/gemini_generated.json, and append a Description block to <inputdir>/data.txt only if no Description already exists.
|
|
|
|
What it does:
|
|
|
|
- Collects up to N frames from the input dir (images -> 1 frame, videos -> up to 3 frames depending on duration).
|
|
- Builds a JSON request containing the prompt and inline base64-encoded images using a Python helper to avoid ARG_MAX issues.
|
|
- Calls the Gemini API and saves the returned JSON text to <inputdir>/gemini_generated.json for review.
|
|
- Appends only a Description block ("Description: |\n ...") to <inputdir>/data.txt if data.txt does not already contain a Description key (case-insensitive). It never changes other keys in data.txt.
|
|
- Leaves gemini_generated.json unchanged (always written as the raw response).
|
|
|
|
Configuration and requirements:
|
|
|
|
- GEMINI_API_KEY should be placed in /etc/mvlog/gemini.env and be readable by www-data (recommended owner root:www-data mode 640).
|
|
- Required tools on the webserver: jq, python3, ffmpeg, ffprobe, curl.
|
|
- Run the script as the webserver user so created files are owned by www-data and permissions are correct:
|
|
|
|
sudo -u www-data /var/www/html/mvlog/bin/generate_data.sh /var/www/html/mvlog/in-dir/20230630_napoli "Naples" "Naples, Italy" 8
|
|
|
|
Troubleshooting:
|
|
|
|
- If Gemini returns HTTP 429 RESOURCE_EXHAUSTED, ensure the API key is attached to a project with billing and the Generative Language API quota enabled.
|
|
- If you see "Argument list too long" errors, ensure generate_data.sh is the deployed version that uses the Python builder (it avoids embedding large base64 arguments directly into jq/curl).
|
|
- Run the script as www-data to avoid permission problems reading /etc/mvlog/gemini.env and to create files owned by the webserver.
|