1219bebb22
Tool to compare json mp4 vs list from any remote you desire to find missings on remote
394 lines
14 KiB
HTML
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 '&';
|
|
if (m === '<') return '<';
|
|
if (m === '>') return '>';
|
|
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> |