- Done. The media platform now tracks sources as either local uploads or remote URLs.

Changed:
[includes/db.php (line 
43)](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php:43) 
adds source_type and source_url, with an automatic migration for 
existing SQLite databases.
[admin/add.php (line 
260)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/add.php:260) 
and [admin/edit.php (line 
383)](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php:383) 
now let each source be an upload or an external URL.
[index.php (line 
226)](/Users/tyemeclifford/Documents/GH/mediaplayer/index.php:226) and 
[embed.php (line 
193)](/Users/tyemeclifford/Documents/GH/mediaplayer/embed.php:193) 
resolve playback URLs based on where the source lives.
[catalogue.php (line 
79)](/Users/tyemeclifford/Documents/GH/mediaplayer/catalogue.php:79) now 
has public search for published videos only.
[README.md (line 
63)](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md:63) 
documents local vs remote sources and public search.
This commit is contained in:
Ty Clifford
2026-06-24 11:58:29 -04:00
parent 7c33de810f
commit b24f0dc416
9 changed files with 392 additions and 98 deletions
+158 -48
View File
@@ -24,10 +24,12 @@ if (isset($_GET['del_src'])) {
$src->execute([$sid, $id]);
$s = $src->fetch();
if ($s) {
$f = MEDIA_DIR . $s['file_path'];
if (file_exists($f)) unlink($f);
if (source_is_local($s)) {
$f = MEDIA_DIR . $s['file_path'];
if ($s['file_path'] && file_exists($f)) unlink($f);
}
$db->prepare("DELETE FROM video_sources WHERE id=?")->execute([$sid]);
$msg = 'Source file removed.';
$msg = 'Source removed.';
}
$video = get_video_by_id($id); // refresh
}
@@ -78,48 +80,108 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$upd_labels = $_POST['src_label'] ?? [];
$upd_quality = $_POST['src_quality'] ?? [];
$upd_sort = $_POST['src_sort'] ?? [];
$upd_urls = $_POST['src_url'] ?? [];
$upd_types = $_POST['src_type'] ?? [];
$upd_ids = $_POST['src_id'] ?? [];
foreach ($upd_ids as $i => $sid) {
$db->prepare("UPDATE video_sources SET label=?, quality=?, sort_order=? WHERE id=? AND video_id=?")
->execute([
$upd_labels[$i] ?? '',
$upd_quality[$i] ?? '',
(int)($upd_sort[$i] ?? $i),
(int)$sid, $id
]);
}
// Add new source files
$new_labels = $_POST['new_source_label'] ?? [];
$new_quality = $_POST['new_source_quality'] ?? [];
$new_sort = $_POST['new_source_sort'] ?? [];
$new_files = $_FILES['new_source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
if (!empty($new_files['tmp_name'])) {
foreach ($new_files['tmp_name'] as $i => $tmp) {
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig = $new_files['name'][$i];
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_video)) continue;
$fname = $slug . '_' . time() . '_' . $i . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$db->prepare("INSERT INTO video_sources (video_id,label,file_path,mime_type,quality,sort_order) VALUES (?,?,?,?,?,?)")
->execute([$id,
trim($new_labels[$i] ?? ''),
'videos/' . $fname,
mime_from_ext($fname),
trim($new_quality[$i] ?? '720p'),
(int)($new_sort[$i] ?? $i),
]);
$source_type = ($upd_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
$source_url = '';
if ($source_type === 'remote') {
$source_url = normalize_media_url($upd_urls[$i] ?? '');
if ($source_url === '') {
$error = 'External video URLs must start with http:// or https://.';
break;
}
}
$params = [
$upd_labels[$i] ?? '',
$upd_quality[$i] ?? '',
(int)($upd_sort[$i] ?? $i),
];
$sql = "UPDATE video_sources SET label=?, quality=?, sort_order=?";
if ($source_type === 'remote') {
$sql .= ", source_url=?, mime_type=?";
$params[] = $source_url;
$params[] = mime_from_ext($source_url);
}
$sql .= " WHERE id=? AND video_id=?";
$params[] = (int)$sid;
$params[] = $id;
$db->prepare($sql)->execute($params);
}
header('Location: edit.php?id=' . $id . '&saved=1');
exit;
// Add new sources
if (!$error) {
$new_types = $_POST['new_source_type'] ?? [];
$new_labels = $_POST['new_source_label'] ?? [];
$new_quality = $_POST['new_source_quality'] ?? [];
$new_sort = $_POST['new_source_sort'] ?? [];
$new_urls = $_POST['new_source_url'] ?? [];
$new_files = $_FILES['new_source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
$new_count = max(
count($new_types),
count($new_labels),
count($new_urls),
isset($new_files['tmp_name']) && is_array($new_files['tmp_name']) ? count($new_files['tmp_name']) : 0
);
$moved_files = [];
$inserted_source_ids = [];
for ($i = 0; $i < $new_count; $i++) {
$type = ($new_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local';
$label = trim($new_labels[$i] ?? '');
$quality = trim($new_quality[$i] ?? '720p');
$sort = (int)($new_sort[$i] ?? $i);
if ($type === 'remote') {
$url = normalize_media_url($new_urls[$i] ?? '');
if ($url === '') {
if (trim($new_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.';
continue;
}
$db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'remote', '', $url, mime_from_ext($url), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId();
continue;
}
$tmp = $new_files['tmp_name'][$i] ?? '';
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig = $new_files['name'][$i] ?? '';
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_video, true)) continue;
$fname = $slug . '_' . time() . '_' . $i . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$moved_files[] = $videos_dir . $fname;
$db->prepare("
INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?,?,?)
")->execute([$id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]);
$inserted_source_ids[] = (int)$db->lastInsertId();
}
}
if ($error) {
foreach ($inserted_source_ids as $inserted_id) {
$db->prepare("DELETE FROM video_sources WHERE id=? AND video_id=?")->execute([$inserted_id, $id]);
}
foreach ($moved_files as $moved) {
if (file_exists($moved)) unlink($moved);
}
} else {
header('Location: edit.php?id=' . $id . '&saved=1');
exit;
}
}
}
}
}
@@ -143,11 +205,18 @@ render_head('Edit Video — Admin', '
.source-item-header { display:flex; align-items:center; justify-content:space-between; gap:.75rem; margin-bottom:.75rem; flex-wrap:wrap; }
.source-item-file { font-family:"Share Tech Mono",monospace; font-size:.68rem; color:var(--text-2); word-break:break-all; }
.source-item-mime { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); }
.source-item-fields { display:grid; grid-template-columns:1fr 1fr 80px; gap:.6rem; }
@media(max-width:600px) { .source-item-fields { grid-template-columns:1fr 1fr; } }
.source-item-fields { display:grid; grid-template-columns:1.5fr 1fr 1fr 80px; gap:.6rem; }
.source-item-fields .span4 { grid-column:1/-1; }
@media(max-width:760px) { .source-item-fields { grid-template-columns:1fr 1fr; } }
@media(max-width:560px) { .source-item-fields { grid-template-columns:1fr; } }
.source-badge { display:inline-flex; align-items:center; padding:.16em .5em; border-radius:99px; font-family:"Share Tech Mono",monospace; font-size:.56rem; letter-spacing:.08em; text-transform:uppercase; border:1px solid var(--border-2); color:var(--text-3); margin-bottom:.25rem; }
.source-badge-remote { color:var(--blue); border-color:rgba(0,200,255,.35); background:rgba(0,200,255,.08); }
.source-badge-local { color:var(--green); border-color:rgba(0,232,122,.3); background:rgba(0,232,122,.08); }
.add-source-row { background:var(--surface-2); border:1px solid var(--border-2); border-radius:var(--r); padding:1rem; display:grid; grid-template-columns:1fr 1fr 1fr auto; gap:.75rem; align-items:end; }
@media(max-width:700px) { .add-source-row { grid-template-columns:1fr 1fr; } }
.add-source-row { background:var(--surface-2); border:1px solid var(--border-2); 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) { .add-source-row { grid-template-columns:1fr 1fr; } }
@media(max-width:560px) { .add-source-row { grid-template-columns:1fr; } }
.source-location-panel[hidden] { display:none; }
.admin-layout { display:grid; grid-template-columns:1fr; gap:1.5rem; }
@media(min-width:900px) { .admin-layout { grid-template-columns:220px 1fr; } }
@@ -270,18 +339,30 @@ render_head('Edit Video — Admin', '
<div class="source-item">
<div class="source-item-header">
<div>
<div class="source-item-file"><?= h($s['file_path']) ?></div>
<span class="source-badge <?= source_is_remote($s) ? 'source-badge-remote' : 'source-badge-local' ?>">
<?= source_is_remote($s) ? 'External URL' : 'Uploaded File' ?>
</span>
<div class="source-item-file"><?= h(source_location($s)) ?></div>
<div class="source-item-mime"><?= h($s['mime_type']) ?></div>
</div>
<a href="edit.php?id=<?= $id ?>&del_src=<?= $s['id'] ?>"
class="btn btn-danger btn-sm"
onclick="return confirm('Remove this source file from disk?')">
onclick="return confirm('Remove this source?<?= source_is_local($s) ? ' Uploaded files are also deleted from disk.' : '' ?>')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19 6l-1 14H6L5 6"/><path d="M9 6V4h6v2"/></svg>
Remove
</a>
</div>
<div class="source-item-fields">
<input type="hidden" name="src_id[]" value="<?= $s['id'] ?>">
<input type="hidden" name="src_type[]" value="<?= h($s['source_type'] ?? 'local') ?>">
<?php if (source_is_remote($s)): ?>
<div class="form-group span4">
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="src_url[]" value="<?= h($s['source_url']) ?>">
</div>
<?php else: ?>
<input type="hidden" name="src_url[]" value="">
<?php endif; ?>
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="src_quality[]">
@@ -307,16 +388,27 @@ render_head('Edit Video — Admin', '
<!-- ── Add new sources ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Add New Source Files</p>
<p class="hint" style="margin-bottom:.85rem;">Upload additional formats (MP4, WebM, OGV, MOV, MKV, AVI).</p>
<p class="section-heading">Add New Sources</p>
<p class="hint" style="margin-bottom:.85rem;">Upload another file or link an external URL. Remote URLs must use http:// or https://.</p>
<div class="source-list" id="new-sources-container">
<div class="add-source-row" data-new-row="0">
<div class="form-group">
<label class="form-label">Source</label>
<select class="form-select source-type-select" name="new_source_type[]">
<option value="local" selected>Upload</option>
<option value="remote">External URL</option>
</select>
</div>
<div class="form-group source-location-panel source-location-local">
<label class="form-label">Video File</label>
<input class="form-input" type="file" name="new_source_file[]"
accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;">
</div>
<div class="form-group source-location-panel source-location-remote" hidden>
<label class="form-label">External URL</label>
<input class="form-input" type="url" name="new_source_url[]" placeholder="https://example.com/video.mp4">
</div>
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="new_source_quality[]">
@@ -340,7 +432,7 @@ render_head('Edit Video — Admin', '
</div>
<button type="button" id="add-new-source" class="btn btn-secondary btn-sm" 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 Format
Add Another Source
</button>
</div>
@@ -359,6 +451,19 @@ render_head('Edit Video — Admin', '
</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; });
}
document.querySelectorAll('[data-new-row]').forEach(function(row) {
toggleSourceRow(row);
var select = row.querySelector('.source-type-select');
if (select) select.addEventListener('change', function(){ toggleSourceRow(row); });
});
document.getElementById('add-new-source').addEventListener('click', function() {
var container = document.getElementById('new-sources-container');
var rows = container.querySelectorAll('[data-new-row]');
@@ -367,13 +472,18 @@ document.getElementById('add-new-source').addEventListener('click', function() {
clone.setAttribute('data-new-row', idx);
clone.querySelectorAll('input[type="file"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="text"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="url"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="number"]').forEach(function(el){ el.value=idx; });
clone.querySelectorAll('.source-type-select').forEach(function(el){ el.value='local'; });
if (!clone.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-new-row]').remove(); };
clone.appendChild(btn);
}
toggleSourceRow(clone);
var select = clone.querySelector('.source-type-select');
if (select) select.addEventListener('change', function(){ toggleSourceRow(clone); });
container.appendChild(clone);
});
</script>