7deae1a399
What changed: Added uploaded/direct audio playback paths in [index.php](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php) and [embed.php](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php). Added common audio upload support: MP3, M4A, AAC, WAV, FLAC, OGG, OGA, OPUS. Added waveform poster generation for uploaded audio via ffmpeg, stored like normal thumbnails under media/thumbs/. Updated add/edit forms to accept media files, not just video files. Added audio-friendly labels like Audio, 320kbps, 256kbps, 128kbps. Updated docs and changelog.
501 lines
25 KiB
PHP
501 lines
25 KiB
PHP
<?php
|
|
require_once __DIR__ . '/auth.php';
|
|
require_once __DIR__ . '/../includes/layout.php';
|
|
admin_require_auth();
|
|
|
|
$db = get_db();
|
|
$error = '';
|
|
$msg = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$title = trim($_POST['title'] ?? '');
|
|
$description = trim($_POST['description'] ?? '');
|
|
$duration = (int)($_POST['duration'] ?? 0);
|
|
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
|
$published = isset($_POST['published']) ? 1 : 0;
|
|
$auto_thumb = isset($_POST['auto_thumbnail']);
|
|
$source_types = $_POST['source_type'] ?? [];
|
|
$source_labels = $_POST['source_label'] ?? [];
|
|
$source_quality = $_POST['source_quality'] ?? [];
|
|
$source_order = $_POST['source_sort'] ?? [];
|
|
$source_urls = $_POST['source_url'] ?? [];
|
|
$source_files = $_FILES['source_file'] ?? [];
|
|
$source_metadata = [];
|
|
$initial_external_view_count = 0;
|
|
|
|
$source_count = max(
|
|
count($source_types),
|
|
count($source_labels),
|
|
count($source_urls),
|
|
isset($source_files['tmp_name']) && is_array($source_files['tmp_name']) ? count($source_files['tmp_name']) : 0
|
|
);
|
|
|
|
for ($i = 0; $i < $source_count; $i++) {
|
|
$type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
|
|
if ($type !== 'remote') continue;
|
|
|
|
$url = normalize_media_url($source_urls[$i] ?? '');
|
|
if ($url === '') continue;
|
|
|
|
$metadata = media_external_metadata($url);
|
|
$source_metadata[$url] = $metadata;
|
|
if ($title === '' && $metadata['title'] !== '') $title = $metadata['title'];
|
|
if ($description === '' && $metadata['description'] !== '') $description = $metadata['description'];
|
|
if ($duration <= 0 && (int)$metadata['duration'] > 0) $duration = (int)$metadata['duration'];
|
|
if ((int)$metadata['view_count'] > $initial_external_view_count) {
|
|
$initial_external_view_count = (int)$metadata['view_count'];
|
|
}
|
|
}
|
|
|
|
$_POST['title'] = $title;
|
|
$_POST['description'] = $description;
|
|
$_POST['duration'] = (string)$duration;
|
|
|
|
if (!$title) {
|
|
$error = 'Title is required. If you use an external URL, click Fetch Info or leave the title blank and make sure the URL is readable.';
|
|
} else {
|
|
$slug = make_slug($title);
|
|
|
|
// Handle thumbnail upload
|
|
$thumbnail = '';
|
|
if (!empty($_FILES['thumbnail']['tmp_name'])) {
|
|
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
|
|
$allowed = ['jpg','jpeg','png','webp','gif'];
|
|
if (in_array($ext, $allowed)) {
|
|
$thumb_dir = MEDIA_DIR . 'thumbs/';
|
|
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
|
|
$fname = $slug . '_thumb_' . time() . '.' . $ext;
|
|
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
|
|
$thumbnail = 'thumbs/' . $fname;
|
|
}
|
|
} else {
|
|
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
// Insert video
|
|
$db->prepare("
|
|
INSERT INTO videos (title, description, slug, thumbnail, duration, external_view_count, sort_order, published)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
")->execute([$title, $description, $slug, $thumbnail, $duration, $initial_external_view_count, $sort_order, $published]);
|
|
|
|
$video_id = (int)$db->lastInsertId();
|
|
|
|
// Handle local upload and remote URL sources
|
|
$videos_dir = MEDIA_DIR . 'videos/';
|
|
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
|
|
|
|
$inserted_sources = 0;
|
|
$moved_files = [];
|
|
$allowed_media = media_allowed_upload_extensions();
|
|
|
|
for ($i = 0; $i < $source_count; $i++) {
|
|
$type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
|
|
$label = trim($source_labels[$i] ?? '');
|
|
$quality = trim($source_quality[$i] ?? '');
|
|
$sort = (int)($source_order[$i] ?? $i);
|
|
|
|
if ($type === 'remote') {
|
|
$url = normalize_media_url($source_urls[$i] ?? '');
|
|
if ($url === '') {
|
|
if (trim($source_urls[$i] ?? '') !== '') $error = 'External media URLs must start with http:// or https://.';
|
|
continue;
|
|
}
|
|
$metadata = $source_metadata[$url] ?? [];
|
|
if ($label === '' && !empty($metadata['provider'])) {
|
|
$label = ucwords(str_replace('-', ' ', (string)$metadata['provider']));
|
|
}
|
|
|
|
$db->prepare("
|
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
")->execute([$video_id, $label, 'remote', '', $url, source_storage_mime_from_url($url), $quality, $sort]);
|
|
$inserted_sources++;
|
|
continue;
|
|
}
|
|
|
|
$tmp = $source_files['tmp_name'][$i] ?? '';
|
|
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
|
|
$orig_name = $source_files['name'][$i] ?? '';
|
|
$ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, $allowed_media, true)) {
|
|
$error = 'Uploaded media files must be one of: ' . media_upload_format_summary() . '.';
|
|
break;
|
|
}
|
|
|
|
$fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext;
|
|
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
|
|
$target = $videos_dir . $fname;
|
|
$moved_files[] = $target;
|
|
$db->prepare("
|
|
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
")->execute([$video_id, $label, 'local', 'videos/' . $fname, '', media_mime_from_local_file($target, $fname), $quality, $sort]);
|
|
$inserted_sources++;
|
|
}
|
|
}
|
|
|
|
if ($error) {
|
|
foreach ($moved_files as $moved) {
|
|
if (file_exists($moved)) unlink($moved);
|
|
}
|
|
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
|
|
$db->prepare("DELETE FROM videos WHERE id=?")->execute([$video_id]);
|
|
} else {
|
|
if ($thumbnail === '' && $auto_thumb) {
|
|
$thumbnail = media_auto_thumbnail_for_video($video_id, $slug);
|
|
if ($thumbnail !== '') {
|
|
$db->prepare("UPDATE videos SET thumbnail=? WHERE id=?")->execute([$thumbnail, $video_id]);
|
|
}
|
|
}
|
|
media_apply_youtube_timestamp($video_id);
|
|
|
|
header('Location: edit.php?id=' . $video_id . '&saved=1');
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
render_head('Add Media — Admin', '
|
|
.form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; }
|
|
@media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } }
|
|
.form-grid .span2 { grid-column:1/-1; }
|
|
|
|
.sources-section { display:flex; flex-direction:column; gap:.85rem; }
|
|
.source-row {
|
|
background:var(--surface-2); border:1px solid var(--border);
|
|
border-radius:var(--r); padding:1rem;
|
|
display:grid; grid-template-columns:140px minmax(220px,1.5fr) 1fr 1fr 80px auto;
|
|
gap:.75rem; align-items:end;
|
|
}
|
|
@media(max-width:900px) { .source-row { grid-template-columns:1fr 1fr; } }
|
|
@media(max-width:560px) { .source-row { grid-template-columns:1fr; } }
|
|
.add-source-btn { align-self:flex-start; }
|
|
.source-location-panel[hidden] { display:none; }
|
|
.source-url-control { display:flex; gap:.45rem; align-items:center; }
|
|
.source-url-control input { min-width:0; }
|
|
.metadata-status { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); min-height:1em; }
|
|
.metadata-status.success { color:var(--green); }
|
|
.metadata-status.error { color:var(--red); }
|
|
|
|
.section-divider {
|
|
border:none; border-top:1px solid var(--border); margin:1.5rem 0 .5rem;
|
|
}
|
|
.section-heading {
|
|
font-family:"Share Tech Mono",monospace; font-size:.65rem;
|
|
letter-spacing:.15em; text-transform:uppercase; color:var(--text-3);
|
|
margin-bottom:.75rem;
|
|
}
|
|
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; }
|
|
|
|
.admin-layout { display:grid; grid-template-columns:1fr; gap:1.5rem; }
|
|
@media(min-width:900px) { .admin-layout { grid-template-columns:220px 1fr; } }
|
|
.admin-nav {
|
|
background:var(--surface); border:1px solid var(--border);
|
|
border-radius:calc(var(--r)+2px); padding:1.2rem;
|
|
display:flex; flex-direction:column; gap:.4rem;
|
|
align-self:start; position:sticky; top:1rem;
|
|
}
|
|
.admin-nav-label { font-family:"Share Tech Mono",monospace; font-size:.58rem; letter-spacing:.18em; text-transform:uppercase; color:var(--text-3); margin-bottom:.3rem; padding-left:.5rem; }
|
|
.admin-nav a { display:flex; align-items:center; gap:.6rem; padding:.55rem .75rem; border-radius:var(--r); font-size:.82rem; color:var(--text-2); text-decoration:none; transition:background .15s,color .15s; }
|
|
.admin-nav a:hover { background:rgba(255,255,255,.05); color:var(--text); }
|
|
.admin-nav a.active { background:rgba(176,96,255,.12); color:var(--purple); }
|
|
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
|
|
');
|
|
?>
|
|
<main class="page" role="main">
|
|
<header class="topbar">
|
|
<div class="topbar-brand">
|
|
<div>
|
|
<div class="brand-name">Ty Clifford</div>
|
|
<div class="brand-sub">admin / add media</div>
|
|
</div>
|
|
</div>
|
|
<nav class="topbar-social">
|
|
<a class="social-pill" href="<?= h(public_absolute_url(public_home_url())) ?>" target="_blank">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
|
View Site
|
|
</a>
|
|
<a class="social-pill" href="logout.php">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16,17 21,12 16,7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|
Logout
|
|
</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="notice notice-error"><?= h($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="admin-layout">
|
|
<aside class="admin-nav">
|
|
<div class="admin-nav-label">Navigation</div>
|
|
<a href="index.php">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
Videos
|
|
</a>
|
|
<a href="add.php" class="active">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
|
Add Media
|
|
</a>
|
|
<a href="youtube_import.php">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22.54 6.42a2.78 2.78 0 00-1.95-2C18.88 4 12 4 12 4s-6.88 0-8.59.46a2.78 2.78 0 00-1.95 2A29 29 0 001 12a29 29 0 00.46 5.58 2.78 2.78 0 001.95 2C5.12 20 12 20 12 20s6.88 0 8.59-.46a2.78 2.78 0 001.95-2A29 29 0 0023 12a29 29 0 00-.46-5.58z"/><polygon points="10,15 15,12 10,9"/></svg>
|
|
YouTube Importer
|
|
</a>
|
|
<hr class="nav-divider">
|
|
<a href="settings.php">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M4.93 4.93l1.41 1.41M19.07 19.07l-1.41-1.41M4.93 19.07l1.41-1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
|
|
Settings
|
|
</a>
|
|
</aside>
|
|
|
|
<div>
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;">
|
|
<div>
|
|
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">New Entry</p>
|
|
<h1 style="font-size:1.05rem;font-weight:700;color:var(--text);">Add Media</h1>
|
|
</div>
|
|
<a href="index.php" class="btn btn-secondary btn-sm">← Back</a>
|
|
</div>
|
|
|
|
<form method="post" action="add.php" enctype="multipart/form-data">
|
|
|
|
<!-- ── Video details ── -->
|
|
<div class="card" style="margin-bottom:1rem;">
|
|
<p class="section-heading">Media Details</p>
|
|
<div class="form-grid">
|
|
<div class="form-group span2">
|
|
<label class="form-label" for="title">Title</label>
|
|
<input class="form-input" type="text" id="title" name="title"
|
|
value="<?= h($_POST['title'] ?? '') ?>" placeholder="Auto-filled from external URL when available">
|
|
<span class="hint">Leave blank when pasting a supported external URL, then use Fetch Info.</span>
|
|
</div>
|
|
<div class="form-group span2">
|
|
<label class="form-label" for="description">Description</label>
|
|
<textarea class="form-textarea" id="description" name="description" rows="3"><?= h($_POST['description'] ?? '') ?></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="duration">Duration (seconds)</label>
|
|
<input class="form-input" type="number" id="duration" name="duration"
|
|
value="<?= h($_POST['duration'] ?? '0') ?>" min="0" placeholder="0">
|
|
<span class="hint">e.g. 3600 = 1 hour. Leave 0 if unknown.</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="sort_order">Sort Order</label>
|
|
<input class="form-input" type="number" id="sort_order" name="sort_order"
|
|
value="<?= h($_POST['sort_order'] ?? '0') ?>" min="0">
|
|
<span class="hint">Lower numbers appear first.</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="thumbnail">Thumbnail Image</label>
|
|
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
|
|
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
|
|
<label style="display:flex;align-items:center;gap:.55rem;margin-top:.55rem;cursor:pointer;">
|
|
<input type="checkbox" name="auto_thumbnail" value="1" <?= isset($_POST['auto_thumbnail']) || $_SERVER['REQUEST_METHOD'] !== 'POST' ? 'checked' : '' ?>>
|
|
<span style="font-size:.78rem;color:var(--text-2);">Auto-grab thumbnail or waveform from source if no image is uploaded</span>
|
|
</label>
|
|
<span class="hint">JPG, PNG, WebP or GIF. Auto-grab supports providers, local video first frames, and local audio waveforms when ffmpeg is available.</span>
|
|
</div>
|
|
<div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;">
|
|
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;">
|
|
<input type="checkbox" name="published" value="1" <?= isset($_POST['published']) || !isset($_POST['title']) ? 'checked' : '' ?>>
|
|
<span class="form-label" style="margin:0">Published</span>
|
|
</label>
|
|
<span class="hint">Uncheck to save as draft.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Media sources ── -->
|
|
<div class="card">
|
|
<p class="section-heading">Media Sources</p>
|
|
<p class="hint" style="margin-bottom:1rem;">
|
|
Upload a video or audio file, link a direct media file, or paste a provider page URL such as YouTube or Vimeo. The player will use a provider embed when the URL supports it.
|
|
Uploaded files: <strong><?= h(media_upload_format_summary()) ?></strong>. Remote URLs must use <strong>http://</strong> or <strong>https://</strong>.
|
|
</p>
|
|
|
|
<div class="sources-section" id="sources-container">
|
|
<!-- Row template (JS clones this) -->
|
|
<div class="source-row" data-source-row="0">
|
|
<div class="form-group">
|
|
<label class="form-label">Source</label>
|
|
<select class="form-select source-type-select" name="source_type[]">
|
|
<option value="local" selected>Upload</option>
|
|
<option value="remote">External URL / Embed</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group source-location-panel source-location-local">
|
|
<label class="form-label">Media File</label>
|
|
<input class="form-input" type="file" name="source_file[]"
|
|
accept="<?= h(media_upload_accept_attribute()) ?>"
|
|
style="padding:.45rem .6rem;">
|
|
</div>
|
|
<div class="form-group source-location-panel source-location-remote" hidden>
|
|
<label class="form-label">External URL</label>
|
|
<div class="source-url-control">
|
|
<input class="form-input source-url-input" type="url" name="source_url[]" placeholder="https://www.youtube.com/watch?v=...">
|
|
<button type="button" class="btn btn-secondary btn-sm fetch-info-btn">Fetch Info</button>
|
|
</div>
|
|
<span class="metadata-status" aria-live="polite"></span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Quality Label</label>
|
|
<select class="form-select" name="source_quality[]">
|
|
<option value="1080p">1080p</option>
|
|
<option value="720p" selected>720p</option>
|
|
<option value="480p">480p</option>
|
|
<option value="360p">360p</option>
|
|
<option value="4K">4K</option>
|
|
<option value="Audio">Audio</option>
|
|
<option value="320kbps">320kbps</option>
|
|
<option value="256kbps">256kbps</option>
|
|
<option value="128kbps">128kbps</option>
|
|
<option value="">—</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Display Label</label>
|
|
<input class="form-input" type="text" name="source_label[]" placeholder="e.g. HD">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Sort</label>
|
|
<input class="form-input" type="number" name="source_sort[]" value="0" min="0" style="width:70px;">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" class="btn btn-secondary btn-sm add-source-btn" id="add-source" style="margin-top:.75rem;align-self:flex-start;">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
Add Another Source
|
|
</button>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap;">
|
|
<button class="btn btn-primary" type="submit">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
|
|
Save Media
|
|
</button>
|
|
<a href="index.php" class="btn btn-secondary">Cancel</a>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<?php render_footer(); ?>
|
|
</main>
|
|
|
|
<script>
|
|
function toggleSourceRow(row) {
|
|
var select = row.querySelector('.source-type-select');
|
|
var isRemote = select && select.value === 'remote';
|
|
row.querySelectorAll('.source-location-local').forEach(function(el){ el.hidden = isRemote; });
|
|
row.querySelectorAll('.source-location-remote').forEach(function(el){ el.hidden = !isRemote; });
|
|
}
|
|
|
|
function providerLabel(value) {
|
|
if (!value) return '';
|
|
return value.replace(/-/g, ' ').replace(/\b\w/g, function(ch){ return ch.toUpperCase(); });
|
|
}
|
|
|
|
function setMetadataStatus(row, text, type) {
|
|
var status = row.querySelector('.metadata-status');
|
|
if (!status) return;
|
|
status.textContent = text || '';
|
|
status.classList.remove('success', 'error');
|
|
if (type) status.classList.add(type);
|
|
}
|
|
|
|
function fillFromMetadata(row, metadata, force) {
|
|
if (!metadata) return;
|
|
var title = document.getElementById('title');
|
|
var description = document.getElementById('description');
|
|
var duration = document.getElementById('duration');
|
|
var label = row.querySelector('input[name="source_label[]"]');
|
|
|
|
if (metadata.title && title && (force || !title.value.trim())) title.value = metadata.title;
|
|
if (metadata.description && description && (force || !description.value.trim())) description.value = metadata.description;
|
|
if (metadata.duration && duration && (force || !parseInt(duration.value || '0', 10))) duration.value = metadata.duration;
|
|
if (label && !label.value.trim()) label.value = providerLabel(metadata.provider) || metadata.title || 'External';
|
|
}
|
|
|
|
function fetchInfoForRow(row, force) {
|
|
var input = row.querySelector('.source-url-input');
|
|
var url = input ? input.value.trim() : '';
|
|
if (!url) return;
|
|
setMetadataStatus(row, 'Fetching media info...', '');
|
|
|
|
fetch('media_info.php?url=' + encodeURIComponent(url), { credentials: 'same-origin' })
|
|
.then(function(response) {
|
|
if (!response.ok) throw new Error('Request failed');
|
|
return response.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data.ok) {
|
|
setMetadataStatus(row, data.error || 'No metadata found.', 'error');
|
|
return;
|
|
}
|
|
fillFromMetadata(row, data.metadata, force);
|
|
var bits = [];
|
|
if (data.metadata.provider) bits.push(providerLabel(data.metadata.provider));
|
|
if (data.metadata.view_count) bits.push(Number(data.metadata.view_count).toLocaleString() + ' external views');
|
|
setMetadataStatus(row, bits.length ? bits.join(' · ') : 'Media info loaded.', 'success');
|
|
})
|
|
.catch(function() {
|
|
setMetadataStatus(row, 'Unable to fetch media info.', 'error');
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-source-row]').forEach(function(row) {
|
|
toggleSourceRow(row);
|
|
var select = row.querySelector('.source-type-select');
|
|
if (select) select.addEventListener('change', function(){ toggleSourceRow(row); });
|
|
});
|
|
|
|
document.getElementById('sources-container').addEventListener('click', function(event) {
|
|
var btn = event.target.closest('.fetch-info-btn');
|
|
if (!btn) return;
|
|
fetchInfoForRow(btn.closest('[data-source-row]'), true);
|
|
});
|
|
|
|
document.getElementById('sources-container').addEventListener('blur', function(event) {
|
|
if (!event.target.classList.contains('source-url-input')) return;
|
|
var title = document.getElementById('title');
|
|
if (title && title.value.trim()) return;
|
|
fetchInfoForRow(event.target.closest('[data-source-row]'), false);
|
|
}, true);
|
|
|
|
document.getElementById('add-source').addEventListener('click', function() {
|
|
var container = document.getElementById('sources-container');
|
|
var rows = container.querySelectorAll('[data-source-row]');
|
|
var idx = rows.length;
|
|
var first = rows[0].cloneNode(true);
|
|
first.setAttribute('data-source-row', idx);
|
|
// Clear file inputs (can't clone values for file inputs)
|
|
first.querySelectorAll('input[type="file"]').forEach(function(el){ el.value=''; });
|
|
first.querySelectorAll('input[type="text"]').forEach(function(el){ el.value=''; });
|
|
first.querySelectorAll('input[type="url"]').forEach(function(el){ el.value=''; });
|
|
first.querySelectorAll('input[type="number"]').forEach(function(el){ el.value=idx; });
|
|
first.querySelectorAll('.metadata-status').forEach(function(el){ el.textContent=''; el.classList.remove('success','error'); });
|
|
first.querySelectorAll('.source-type-select').forEach(function(el){ el.value='local'; });
|
|
// Add remove button if not already there
|
|
if (!first.querySelector('.remove-row')) {
|
|
var btn = document.createElement('button');
|
|
btn.type='button';
|
|
btn.className='btn btn-danger btn-sm remove-row';
|
|
btn.style='align-self:flex-end';
|
|
btn.innerHTML='✕';
|
|
btn.onclick=function(){ this.closest('[data-source-row]').remove(); };
|
|
first.appendChild(btn);
|
|
}
|
|
toggleSourceRow(first);
|
|
var select = first.querySelector('.source-type-select');
|
|
if (select) select.addEventListener('change', function(){ toggleSourceRow(first); });
|
|
container.appendChild(first);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|