Files
YoutubeArchiveSuite/vidcompareremote.html
Alcea 1219bebb22 Add files via upload
Tool to compare json mp4 vs list from any remote you desire to find missings on remote
2026-04-05 19:46:06 +02:00

394 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video ID Comparator - Bidirectional</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
color: #333;
border-bottom: 3px solid #0078d4;
padding-bottom: 10px;
}
.config-section {
background: #e8f0fe;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.config-section label {
font-weight: bold;
}
.config-section input {
flex: 1;
min-width: 300px;
padding: 8px 12px;
font-family: monospace;
font-size: 13px;
border: 1px solid #ccc;
border-radius: 6px;
}
.config-section button {
background: #0078d4;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
}
.config-section button:hover {
background: #005a9e;
}
.status {
background: #f0f0f0;
padding: 10px 15px;
border-radius: 8px;
margin: 15px 0;
font-family: monospace;
}
.status.error {
background: #ffe6e6;
color: #c00;
}
.status.success {
background: #e6ffe6;
color: #2a6e2a;
}
.main-panel {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.input-panel {
flex: 1;
min-width: 300px;
}
.results-panel {
flex: 1;
min-width: 300px;
}
textarea {
width: 100%;
height: 350px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 13px;
border: 2px solid #ddd;
border-radius: 8px;
resize: vertical;
}
button {
background: #0078d4;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
margin-right: 10px;
}
button:hover {
background: #005a9e;
}
.stats {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.result-list {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
max-height: 500px;
overflow-y: auto;
}
.result-item {
padding: 10px 15px;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.result-item.missing-from-json {
background: #ffe6e6;
border-left: 4px solid #d32f2f;
}
.result-item.missing-from-paste {
background: #fff3e0;
border-left: 4px solid #ff9800;
}
.result-item.found {
background: #e6ffe6;
border-left: 4px solid #4caf50;
}
.result-item .id {
font-family: monospace;
font-weight: bold;
}
.result-item .title {
color: #666;
font-size: 12px;
margin-top: 4px;
}
.section-header {
background: #f8f9fa;
padding: 8px 12px;
font-weight: bold;
border-bottom: 1px solid #ddd;
position: sticky;
top: 0;
}
hr {
margin: 20px 0;
}
@media (max-width: 768px) {
.main-panel {
flex-direction: column;
}
}
</style>
</head>
<body>
<h1>📹 Video ID Comparator (Bidirectional)</h1>
<p>Shows you what's missing from BOTH sides!</p>
<div class="config-section">
<label>🔗 JSON URL:</label>
<input type="text" id="jsonUrl" value="https://alcea-wisteria.de/videos.json">
<button id="fetchJsonBtn">📥 Load / Reload</button>
</div>
<div id="status" class="status loading">⏳ Click "Load / Reload" to fetch JSON...</div>
<div class="main-panel">
<div class="input-panel">
<h3>📝 Paste your text (file list) here</h3>
<textarea id="inputText" placeholder="Paste your directory listing or file list with .mp4 filenames..."></textarea>
<button id="compareBtn">🔍 Compare (Find Missing)</button>
</div>
<div class="results-panel">
<h3>📊 Comparison Results</h3>
<div id="stats" class="stats">
<span>Waiting for input...</span>
</div>
<div id="resultsList" class="result-list">
<div style="padding: 20px; text-align: center; color: #999;">Load JSON, paste text, then click "Compare"</div>
</div>
</div>
</div>
<script>
let videoDatabase = [];
let videoMap = new Map();
let jsonLoaded = false;
const jsonUrlInput = document.getElementById('jsonUrl');
const fetchBtn = document.getElementById('fetchJsonBtn');
const statusDiv = document.getElementById('status');
const compareBtn = document.getElementById('compareBtn');
const inputTextarea = document.getElementById('inputText');
const statsDiv = document.getElementById('stats');
const resultsListDiv = document.getElementById('resultsList');
async function fetchVideosJson() {
const url = jsonUrlInput.value.trim();
if (!url) {
statusDiv.textContent = '❌ Please enter a JSON URL';
statusDiv.className = 'status error';
return;
}
statusDiv.textContent = `📡 Fetching ${url} ...`;
statusDiv.className = 'status loading';
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!Array.isArray(data)) throw new Error('JSON is not an array');
videoDatabase = data;
videoMap.clear();
for (const item of videoDatabase) {
if (item.id) videoMap.set(item.id, item);
if (item.file) {
const idFromFile = item.file.replace(/\.mp4$/i, '');
if (!videoMap.has(idFromFile)) videoMap.set(idFromFile, item);
}
}
jsonLoaded = true;
statusDiv.textContent = `✅ Loaded ${videoDatabase.length} videos from ${url}`;
statusDiv.className = 'status success';
} catch (error) {
statusDiv.textContent = `❌ Failed: ${error.message}`;
statusDiv.className = 'status error';
jsonLoaded = false;
}
}
// Extract 11-character .mp4 IDs from text
function extractVideoIds(text) {
const regex = /([a-zA-Z0-9_-]{11})\.mp4/gi;
const matches = [];
let match;
while ((match = regex.exec(text)) !== null) {
matches.push(match[1]);
}
return [...new Set(matches)]; // unique
}
function compareBidirectional(pastedIds) {
const jsonIds = videoDatabase.map(v => v.id);
const pastedSet = new Set(pastedIds);
const jsonSet = new Set(jsonIds);
// IDs in paste but NOT in JSON
const missingFromJson = pastedIds.filter(id => !jsonSet.has(id));
// IDs in JSON but NOT in paste
const missingFromPaste = jsonIds.filter(id => !pastedSet.has(id));
// IDs found in both
const found = pastedIds.filter(id => jsonSet.has(id));
return {
missingFromJson,
missingFromPaste,
found,
totalPasted: pastedIds.length,
totalJson: jsonIds.length
};
}
function renderResults(results) {
const { missingFromJson, missingFromPaste, found, totalPasted, totalJson } = results;
// Stats HTML
statsDiv.innerHTML = `
<strong>📊 SUMMARY</strong><br>
📁 Your paste: <strong>${totalPasted}</strong> files<br>
📜 JSON database: <strong>${totalJson}</strong> files<br>
<hr style="margin: 8px 0;">
✅ Found in both: <strong style="color:#4caf50;">${found.length}</strong><br>
❌ <span style="color:#d32f2f;">In your paste but NOT in JSON: ${missingFromJson.length}</span><br>
🟠 <span style="color:#ff9800;">In JSON but NOT in your paste: ${missingFromPaste.length}</span>
`;
let resultsHtml = '';
// MOST IMPORTANT: What's in JSON but missing from your paste
if (missingFromPaste.length > 0) {
resultsHtml += `<div class="section-header" style="background:#fff3e0;">🟠 MISSING FROM YOUR PASTE (${missingFromPaste.length}) - You need to download these!</div>`;
for (const id of missingFromPaste) {
const video = videoMap.get(id);
resultsHtml += `
<div class="result-item missing-from-paste">
<div class="id">🟠 ${id}.mp4</div>
<div class="title">📌 ${escapeHtml(video?.title || 'Unknown title')}<br>👤 ${escapeHtml(video?.uploader || 'Unknown')}</div>
</div>
`;
}
}
// What's in paste but missing from JSON
if (missingFromJson.length > 0) {
resultsHtml += `<div class="section-header" style="background:#ffe6e6; margin-top:10px;">❌ IN YOUR PASTE BUT NOT IN JSON (${missingFromJson.length})</div>`;
for (const id of missingFromJson) {
resultsHtml += `
<div class="result-item missing-from-json">
<div class="id">❌ ${id}.mp4</div>
<div class="title">⚠️ Not found in JSON database</div>
</div>
`;
}
}
// Found entries (optional - collapse by default if too many)
if (found.length > 0 && found.length <= 50) {
resultsHtml += `<div class="section-header" style="background:#e6ffe6; margin-top:10px;">✅ FOUND IN BOTH (${found.length})</div>`;
for (const id of found.slice(0, 30)) {
const video = videoMap.get(id);
resultsHtml += `
<div class="result-item found">
<div class="id">✅ ${id}.mp4</div>
<div class="title">${escapeHtml(video?.title || '')}</div>
</div>
`;
}
if (found.length > 30) {
resultsHtml += `<div style="padding: 10px; text-align: center; color: #666;">... and ${found.length - 30} more</div>`;
}
} else if (found.length > 50) {
resultsHtml += `<div class="section-header" style="background:#e6ffe6; margin-top:10px;">✅ FOUND IN BOTH (${found.length}) - too many to list</div>`;
}
if (missingFromPaste.length === 0 && missingFromJson.length === 0) {
resultsHtml = '<div style="padding: 20px; text-align: center; color: #4caf50;">✅ Perfect match! All files are in both places.</div>';
}
resultsListDiv.innerHTML = resultsHtml || '<div style="padding: 20px; text-align: center; color: #999;">No IDs found in your paste.</div>';
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function performComparison() {
if (!jsonLoaded) {
alert('Please load JSON first (click "Load / Reload")');
return;
}
const text = inputTextarea.value;
if (!text.trim()) {
alert('Please paste your file list first');
return;
}
const pastedIds = extractVideoIds(text);
if (pastedIds.length === 0) {
statsDiv.innerHTML = '<span>⚠️ No .mp4 filenames found in your paste. Make sure they have 11-character IDs like "lctjRhK60hk.mp4"</span>';
resultsListDiv.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">No IDs extracted.</div>';
return;
}
const results = compareBidirectional(pastedIds);
renderResults(results);
}
fetchBtn.addEventListener('click', fetchVideosJson);
compareBtn.addEventListener('click', performComparison);
// Auto-load on page load
fetchVideosJson();
</script>
</body>
</html>