Admin: add Describe UI, synchronous Gemini generator endpoint; update generate_data.sh (no-append)
This commit is contained in:
parent
c8a51488c0
commit
c2d64cb00a
6 changed files with 634 additions and 0 deletions
143
lib/generate_data.php
Normal file
143
lib/generate_data.php
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
// lib/generate_data.php
|
||||
// Trigger the bin/generate_data.sh script for a given in-dir job. Requires admin login.
|
||||
require __DIR__ . '/../auth.php';
|
||||
mvlog_require_login();
|
||||
|
||||
$MVLOG_ROOT = dirname(__DIR__);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$job = $_POST['job'] ?? '';
|
||||
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
|
||||
|
||||
if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'invalid job']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jobdir = $MVLOG_ROOT . '/in-dir/' . $job;
|
||||
if (!is_dir($jobdir)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status'=>'error','message'=>'job not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read existing data.txt for Title and Location (case-insensitive aliases)
|
||||
$data_file = $jobdir . '/data.txt';
|
||||
$title = '';
|
||||
$location = '';
|
||||
if (is_file($data_file)) {
|
||||
$lines = file($data_file, FILE_IGNORE_NEW_LINES);
|
||||
$i = 0;
|
||||
while ($i < count($lines)) {
|
||||
$raw = $lines[$i];
|
||||
$line = trim($raw);
|
||||
if ($line === '' || str_starts_with($line, '#')) { $i++; continue; }
|
||||
if (!str_contains($line, ':')) { $i++; continue; }
|
||||
[$key, $value] = array_map('trim', explode(':', $line, 2));
|
||||
$low = strtolower($key);
|
||||
if ($value === '|' && in_array($low, ['description','desc','synopsis'], true)) {
|
||||
// Skip indented block
|
||||
$i++;
|
||||
while ($i < count($lines)) {
|
||||
$next = $lines[$i];
|
||||
if (trim($next) !== '' && $next[0] !== ' ' && $next[0] !== "\t") break;
|
||||
$i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (in_array($low, ['title','name'], true) && $title === '') $title = $value;
|
||||
if (in_array($low, ['place','location','where'], true) && $location === '') $location = $value;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
// If no location in data.txt, attempt to extract GPS from media files and reverse-geocode with Nominatim.
|
||||
if ($location === '') {
|
||||
$ffprobe = file_exists($MVLOG_ROOT . '/ffmpeg/ffprobe') ? $MVLOG_ROOT . '/ffmpeg/ffprobe' : trim((string)shell_exec('command -v ffprobe 2>/dev/null'));
|
||||
if ($ffprobe) {
|
||||
$candidates = array_values(array_filter(scandir($jobdir), function($f) use ($jobdir) { return is_file($jobdir . '/' . $f); }));
|
||||
usort($candidates, 'strnatcasecmp');
|
||||
$last_country = '';
|
||||
$exts = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','webm','mkv'];
|
||||
foreach ($candidates as $f) {
|
||||
$ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $exts, true)) continue;
|
||||
$path = $jobdir . '/' . $f;
|
||||
$cmd = escapeshellcmd($ffprobe) . ' -v error -show_format -show_streams -of json ' . escapeshellarg($path) . ' 2>/dev/null';
|
||||
$out = @shell_exec($cmd);
|
||||
if (!$out) continue;
|
||||
$json = json_decode($out, true);
|
||||
if (!is_array($json)) continue;
|
||||
|
||||
$values = [];
|
||||
$collect = function($obj) use (&$collect, &$values) {
|
||||
if (is_array($obj)) {
|
||||
foreach ($obj as $k => $v) {
|
||||
if (is_string($k) && (stripos($k, 'location') !== false || stripos($k, 'gps') !== false)) {
|
||||
if (is_string($v)) $values[] = $v;
|
||||
}
|
||||
$collect($v);
|
||||
}
|
||||
}
|
||||
};
|
||||
$collect($json);
|
||||
if (empty($values)) continue;
|
||||
|
||||
foreach ($values as $val) {
|
||||
$lat = $lon = null;
|
||||
if (preg_match('/([+\\-]\d+(?:\\.\\d+)?)([+\\-]\d+(?:\\.\\d+)?)/', $val, $m)) {
|
||||
$lat = floatval($m[1]); $lon = floatval($m[2]);
|
||||
} elseif (preg_match('/([+\\-]?\\d+(?:\\.\\d+)?)[,;\\s]+([+\\-]?\\d+(?:\\.\\d+)?)/', $val, $m)) {
|
||||
$lat = floatval($m[1]); $lon = floatval($m[2]);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($zoom = 18; $zoom >= 3; $zoom--) {
|
||||
$query = http_build_query([
|
||||
'format' => 'jsonv2',
|
||||
'lat' => sprintf('%.8f', $lat),
|
||||
'lon' => sprintf('%.8f', $lon),
|
||||
'zoom' => $zoom,
|
||||
'addressdetails' => 1,
|
||||
]);
|
||||
$opts = ['http' => ['header' => "User-Agent: movmaker/1.0 (reverse geocoding for local movie metadata)\r\n", 'timeout' => 10]];
|
||||
$ctx = stream_context_create($opts);
|
||||
$url = 'https://nominatim.openstreetmap.org/reverse?' . $query;
|
||||
$resp = @file_get_contents($url, false, $ctx);
|
||||
if (!$resp) continue;
|
||||
$obj = json_decode($resp, true);
|
||||
if (!is_array($obj) || !isset($obj['address'])) continue;
|
||||
$address = $obj['address'];
|
||||
$country = $address['country'] ?? '';
|
||||
$last_country = $country ?: $last_country;
|
||||
$city = '';
|
||||
foreach (['city','town','village'] as $k) { if (!empty($address[$k])) { $city = $address[$k]; break; } }
|
||||
if ($city) {
|
||||
$location = trim($city . ($country ? ', ' . $country : ''));
|
||||
break 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($location === '') $location = $last_country ?: '';
|
||||
}
|
||||
}
|
||||
|
||||
$script = $MVLOG_ROOT . '/bin/generate_data.sh';
|
||||
if (!is_file($script)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'generate_data.sh not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$log = $jobdir . '/generate_data.log';
|
||||
$bash = '/bin/bash';
|
||||
$cmd = escapeshellcmd($bash) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' >> ' . escapeshellarg($log) . ' 2>&1 & echo $!';
|
||||
|
||||
$pid = trim(@shell_exec($cmd));
|
||||
|
||||
echo json_encode(['status' => 'ok', 'pid' => $pid, 'log' => $log, 'message' => 'started']);
|
||||
exit;
|
||||
101
lib/generate_data_sync.php
Normal file
101
lib/generate_data_sync.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
// lib/generate_data_sync.php
|
||||
// Run generate_data.sh synchronously (no-append) and return the generated JSON/description.
|
||||
require __DIR__ . '/../auth.php';
|
||||
mvlog_require_login();
|
||||
|
||||
$MVLOG_ROOT = dirname(__DIR__);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$job = $_POST['job'] ?? '';
|
||||
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
|
||||
|
||||
if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status'=>'error','message'=>'invalid job']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jobdir = $MVLOG_ROOT . '/in-dir/' . $job;
|
||||
if (!is_dir($jobdir)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status'=>'error','message'=>'job not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read existing data.txt for Title and Location (case-insensitive aliases)
|
||||
$data_file = $jobdir . '/data.txt';
|
||||
$title = '';
|
||||
$location = '';
|
||||
if (is_file($data_file)) {
|
||||
$lines = file($data_file, FILE_IGNORE_NEW_LINES);
|
||||
$i = 0;
|
||||
while ($i < count($lines)) {
|
||||
$raw = $lines[$i];
|
||||
$line = trim($raw);
|
||||
if ($line === '' || str_starts_with($line, '#')) { $i++; continue; }
|
||||
if (!str_contains($line, ':')) { $i++; continue; }
|
||||
[$key, $value] = array_map('trim', explode(':', $line, 2));
|
||||
$low = strtolower($key);
|
||||
if ($value === '|' && in_array($low, ['description','desc','synopsis'], true)) {
|
||||
// Skip indented block
|
||||
$i++;
|
||||
while ($i < count($lines)) {
|
||||
$next = $lines[$i];
|
||||
if (trim($next) !== '' && $next[0] !== ' ' && $next[0] !== "\t") break;
|
||||
$i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (in_array($low, ['title','name'], true) && $title === '') $title = $value;
|
||||
if (in_array($low, ['place','location','where'], true) && $location === '') $location = $value;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Run generator synchronously but in no-append mode so it doesn't modify data.txt directly.
|
||||
$script = $MVLOG_ROOT . '/bin/generate_data.sh';
|
||||
if (!is_file($script)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'generate_data.sh not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Build command and execute. Use no-append flag as last argument.
|
||||
$cmd = escapeshellcmd($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg('no-append') . ' 2>&1';
|
||||
|
||||
// Allow long-running
|
||||
@set_time_limit(0);
|
||||
|
||||
exec($cmd, $out_lines, $rc);
|
||||
$log = implode("\n", $out_lines);
|
||||
|
||||
// If gemini_generated.json exists, parse and return
|
||||
$gfile = $jobdir . '/gemini_generated.json';
|
||||
if (is_file($gfile)) {
|
||||
$raw = @file_get_contents($gfile);
|
||||
$json = @json_decode($raw, true);
|
||||
$description = '';
|
||||
$teaser = '';
|
||||
$tags = [];
|
||||
if (is_array($json)) {
|
||||
$description = isset($json['description']) ? (string)$json['description'] : '';
|
||||
$teaser = isset($json['teaser']) ? (string)$json['teaser'] : '';
|
||||
$tags = isset($json['tags']) && is_array($json['tags']) ? $json['tags'] : [];
|
||||
}
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'description' => $description,
|
||||
'teaser' => $teaser,
|
||||
'tags' => $tags,
|
||||
'raw_json' => $json,
|
||||
'log' => $log,
|
||||
'rc' => $rc,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// No gemini file — return error and include log
|
||||
http_response_code(500);
|
||||
echo json_encode(['status'=>'error','message'=>'no gemini output','log'=>$log,'rc'=>$rc], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
Loading…
Add table
Add a link
Reference in a new issue