637 lines
23 KiB
Python
637 lines
23 KiB
Python
|
|
from flask import Flask, render_template, request, jsonify
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
from werkzeug.utils import secure_filename
|
||
|
|
from data_comparator import KSTCoordiComparator
|
||
|
|
|
||
|
|
app = Flask(__name__)
|
||
|
|
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size
|
||
|
|
app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
|
||
|
|
|
||
|
|
# Global variable to store comparison results
|
||
|
|
comparison_results = None
|
||
|
|
comparator_instance = None
|
||
|
|
|
||
|
|
@app.route('/')
|
||
|
|
def index():
|
||
|
|
return render_template('index.html')
|
||
|
|
|
||
|
|
@app.route('/analyze', methods=['POST'])
|
||
|
|
def analyze_data():
|
||
|
|
global comparison_results, comparator_instance
|
||
|
|
|
||
|
|
try:
|
||
|
|
file_path = request.json.get('file_path', 'data/sample-data.xlsx')
|
||
|
|
sheet_filter = request.json.get('sheet_filter', None)
|
||
|
|
|
||
|
|
if not Path(file_path).exists():
|
||
|
|
return jsonify({'error': f'File not found: {file_path}'}), 400
|
||
|
|
|
||
|
|
# Create comparator and analyze
|
||
|
|
comparator_instance = KSTCoordiComparator(file_path)
|
||
|
|
if not comparator_instance.load_data():
|
||
|
|
return jsonify({'error': 'Failed to load Excel data'}), 500
|
||
|
|
|
||
|
|
# Get comparison results with optional sheet filtering
|
||
|
|
comparison_results = comparator_instance.get_comparison_summary(sheet_filter)
|
||
|
|
|
||
|
|
# Get matched items for display
|
||
|
|
categorization = comparator_instance.categorize_mismatches()
|
||
|
|
matched_items = list(categorization['matched_items'])
|
||
|
|
|
||
|
|
# Filter matched items by sheet if specified
|
||
|
|
if sheet_filter and sheet_filter != 'All Sheets':
|
||
|
|
matched_items = [item for item in matched_items if item.source_sheet == sheet_filter]
|
||
|
|
|
||
|
|
# Format matched items for JSON (limit to first 500 for performance)
|
||
|
|
matched_data = []
|
||
|
|
for item in matched_items[:500]:
|
||
|
|
matched_data.append({
|
||
|
|
'title': item.title,
|
||
|
|
'episode': item.episode,
|
||
|
|
'sheet': item.source_sheet,
|
||
|
|
'row': item.row_index + 1,
|
||
|
|
'reason': 'Perfect match'
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add matched data to results
|
||
|
|
comparison_results['matched_data'] = matched_data
|
||
|
|
comparison_results['matched_items_count'] = len(matched_items) # Update count for filtered data
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'results': comparison_results
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
return jsonify({'error': str(e)}), 500
|
||
|
|
|
||
|
|
@app.route('/upload', methods=['POST'])
|
||
|
|
def upload_file():
|
||
|
|
try:
|
||
|
|
if 'file' not in request.files:
|
||
|
|
return jsonify({'error': 'No file provided'}), 400
|
||
|
|
|
||
|
|
file = request.files['file']
|
||
|
|
if file.filename == '':
|
||
|
|
return jsonify({'error': 'No file selected'}), 400
|
||
|
|
|
||
|
|
if file and file.filename.lower().endswith(('.xlsx', '.xls')):
|
||
|
|
# Save uploaded file
|
||
|
|
filename = secure_filename(file.filename)
|
||
|
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||
|
|
file.save(file_path)
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'file_path': file_path,
|
||
|
|
'filename': filename
|
||
|
|
})
|
||
|
|
else:
|
||
|
|
return jsonify({'error': 'Please upload an Excel file (.xlsx or .xls)'}), 400
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
return jsonify({'error': str(e)}), 500
|
||
|
|
|
||
|
|
@app.route('/get_results')
|
||
|
|
def get_results():
|
||
|
|
if comparison_results is None:
|
||
|
|
return jsonify({'error': 'No analysis results available'}), 404
|
||
|
|
return jsonify(comparison_results)
|
||
|
|
|
||
|
|
def create_templates_dir():
|
||
|
|
"""Create templates directory and HTML file"""
|
||
|
|
templates_dir = Path('templates')
|
||
|
|
templates_dir.mkdir(exist_ok=True)
|
||
|
|
|
||
|
|
html_content = '''<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>KST vs Coordi Data Comparison</title>
|
||
|
|
<style>
|
||
|
|
body {
|
||
|
|
font-family: Arial, sans-serif;
|
||
|
|
margin: 0;
|
||
|
|
padding: 20px;
|
||
|
|
background-color: #f5f5f5;
|
||
|
|
}
|
||
|
|
.container {
|
||
|
|
max-width: 1400px;
|
||
|
|
margin: 0 auto;
|
||
|
|
background: white;
|
||
|
|
padding: 20px;
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
|
|
}
|
||
|
|
h1 {
|
||
|
|
text-align: center;
|
||
|
|
color: #333;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
}
|
||
|
|
.file-section {
|
||
|
|
background: #f8f9fa;
|
||
|
|
padding: 20px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
.file-input {
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
}
|
||
|
|
input[type="text"], input[type="file"] {
|
||
|
|
flex: 1;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
input[type="file"] {
|
||
|
|
padding: 6px 8px;
|
||
|
|
}
|
||
|
|
button {
|
||
|
|
padding: 8px 16px;
|
||
|
|
background: #007bff;
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
button:hover {
|
||
|
|
background: #0056b3;
|
||
|
|
}
|
||
|
|
button:disabled {
|
||
|
|
background: #6c757d;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
.loading {
|
||
|
|
text-align: center;
|
||
|
|
color: #666;
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
.tabs {
|
||
|
|
border-bottom: 2px solid #ddd;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
.tab {
|
||
|
|
display: inline-block;
|
||
|
|
padding: 10px 20px;
|
||
|
|
background: #f8f9fa;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-bottom: none;
|
||
|
|
cursor: pointer;
|
||
|
|
margin-right: 5px;
|
||
|
|
border-radius: 4px 4px 0 0;
|
||
|
|
}
|
||
|
|
.tab.active {
|
||
|
|
background: white;
|
||
|
|
border-bottom: 2px solid white;
|
||
|
|
margin-bottom: -2px;
|
||
|
|
}
|
||
|
|
.tab-content {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
.tab-content.active {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
.summary-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 1fr 1fr;
|
||
|
|
gap: 20px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
.summary-card {
|
||
|
|
background: #f8f9fa;
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 6px;
|
||
|
|
border-left: 4px solid #007bff;
|
||
|
|
}
|
||
|
|
.summary-card h3 {
|
||
|
|
margin-top: 0;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
.count-badge {
|
||
|
|
display: inline-block;
|
||
|
|
background: #007bff;
|
||
|
|
color: white;
|
||
|
|
padding: 4px 8px;
|
||
|
|
border-radius: 12px;
|
||
|
|
font-size: 0.9em;
|
||
|
|
margin-left: 10px;
|
||
|
|
}
|
||
|
|
.reconciliation {
|
||
|
|
background: #d4edda;
|
||
|
|
border: 1px solid #c3e6cb;
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-top: 15px;
|
||
|
|
}
|
||
|
|
.reconciliation.mismatch {
|
||
|
|
background: #f8d7da;
|
||
|
|
border-color: #f5c6cb;
|
||
|
|
}
|
||
|
|
table {
|
||
|
|
width: 100%;
|
||
|
|
border-collapse: collapse;
|
||
|
|
margin-top: 10px;
|
||
|
|
}
|
||
|
|
th, td {
|
||
|
|
padding: 10px;
|
||
|
|
text-align: left;
|
||
|
|
border-bottom: 1px solid #ddd;
|
||
|
|
}
|
||
|
|
th {
|
||
|
|
background-color: #f8f9fa;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
tr:hover {
|
||
|
|
background-color: #f5f5f5;
|
||
|
|
}
|
||
|
|
.error {
|
||
|
|
background: #f8d7da;
|
||
|
|
color: #721c24;
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin: 10px 0;
|
||
|
|
}
|
||
|
|
.success {
|
||
|
|
background: #d4edda;
|
||
|
|
color: #155724;
|
||
|
|
padding: 15px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin: 10px 0;
|
||
|
|
}
|
||
|
|
.table-container {
|
||
|
|
max-height: 500px;
|
||
|
|
overflow-y: auto;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<h1>KST vs Coordi Data Comparison Tool</h1>
|
||
|
|
|
||
|
|
<div class="file-section">
|
||
|
|
<div class="file-input">
|
||
|
|
<label for="filePath">Excel File Path:</label>
|
||
|
|
<input type="text" id="filePath" value="data/sample-data.xlsx" placeholder="Enter file path">
|
||
|
|
<button onclick="analyzeData()" id="analyzeBtn">Analyze Data</button>
|
||
|
|
</div>
|
||
|
|
<div class="file-input" style="margin-top: 10px;">
|
||
|
|
<label>Or Upload File:</label>
|
||
|
|
<input type="file" id="fileUpload" accept=".xlsx,.xls" onchange="handleFileUpload()">
|
||
|
|
<button onclick="uploadAndAnalyze()" id="uploadBtn" disabled>Upload & Analyze</button>
|
||
|
|
</div>
|
||
|
|
<div class="file-input" style="margin-top: 10px;">
|
||
|
|
<label for="sheetFilter">Sheet Filter:</label>
|
||
|
|
<select id="sheetFilter" onchange="filterBySheet()" disabled>
|
||
|
|
<option value="All Sheets">All Sheets</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div id="status"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="results" style="display: none;">
|
||
|
|
<div class="tabs">
|
||
|
|
<div class="tab active" onclick="showTab('summary')">Summary</div>
|
||
|
|
<div class="tab" onclick="showTab('different')">Different</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="summary" class="tab-content active">
|
||
|
|
<h3>Matched Items (Same in both KST and Coordi) <span id="matched-count-display" class="count-badge">0</span></h3>
|
||
|
|
<div class="table-container">
|
||
|
|
<table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Korean Title</th>
|
||
|
|
<th>Episode</th>
|
||
|
|
<th>Sheet</th>
|
||
|
|
<th>Row</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="summary-table">
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="different" class="tab-content">
|
||
|
|
<h3>Different Items <span id="different-count-display" class="count-badge">0</span></h3>
|
||
|
|
<div class="table-container">
|
||
|
|
<table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>KST Data</th>
|
||
|
|
<th>Coordi Data</th>
|
||
|
|
<th>Reason</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="different-table">
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
function showTab(tabName) {
|
||
|
|
// Hide all tab contents
|
||
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||
|
|
content.classList.remove('active');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Remove active class from all tabs
|
||
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
||
|
|
tab.classList.remove('active');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Show selected tab content
|
||
|
|
document.getElementById(tabName).classList.add('active');
|
||
|
|
|
||
|
|
// Add active class to clicked tab
|
||
|
|
event.target.classList.add('active');
|
||
|
|
}
|
||
|
|
|
||
|
|
function analyzeData() {
|
||
|
|
const filePath = document.getElementById('filePath').value;
|
||
|
|
const sheetFilter = document.getElementById('sheetFilter').value;
|
||
|
|
const statusDiv = document.getElementById('status');
|
||
|
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
||
|
|
|
||
|
|
if (!filePath.trim()) {
|
||
|
|
statusDiv.innerHTML = '<div class="error">Please enter a file path</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Show loading state
|
||
|
|
statusDiv.innerHTML = '<div class="loading">Analyzing data...</div>';
|
||
|
|
analyzeBtn.disabled = true;
|
||
|
|
analyzeBtn.textContent = 'Analyzing...';
|
||
|
|
|
||
|
|
// Make API call
|
||
|
|
fetch('/analyze', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
file_path: filePath,
|
||
|
|
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.then(response => response.json())
|
||
|
|
.then(data => {
|
||
|
|
if (data.success) {
|
||
|
|
statusDiv.innerHTML = '<div class="success">Analysis complete!</div>';
|
||
|
|
updateResults(data.results);
|
||
|
|
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||
|
|
document.getElementById('results').style.display = 'block';
|
||
|
|
} else {
|
||
|
|
statusDiv.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch(error => {
|
||
|
|
statusDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||
|
|
})
|
||
|
|
.finally(() => {
|
||
|
|
analyzeBtn.disabled = false;
|
||
|
|
analyzeBtn.textContent = 'Analyze Data';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSheetFilter(sheetNames, currentFilter) {
|
||
|
|
const select = document.getElementById('sheetFilter');
|
||
|
|
select.innerHTML = '<option value="All Sheets">All Sheets</option>';
|
||
|
|
|
||
|
|
sheetNames.forEach(sheetName => {
|
||
|
|
const option = document.createElement('option');
|
||
|
|
option.value = sheetName;
|
||
|
|
option.textContent = sheetName;
|
||
|
|
if (sheetName === currentFilter) {
|
||
|
|
option.selected = true;
|
||
|
|
}
|
||
|
|
select.appendChild(option);
|
||
|
|
});
|
||
|
|
|
||
|
|
select.disabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterBySheet() {
|
||
|
|
// Re-analyze with the selected sheet filter
|
||
|
|
analyzeData();
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleFileUpload() {
|
||
|
|
const fileInput = document.getElementById('fileUpload');
|
||
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
||
|
|
|
||
|
|
if (fileInput.files.length > 0) {
|
||
|
|
uploadBtn.disabled = false;
|
||
|
|
uploadBtn.textContent = 'Upload & Analyze';
|
||
|
|
} else {
|
||
|
|
uploadBtn.disabled = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function uploadAndAnalyze() {
|
||
|
|
const fileInput = document.getElementById('fileUpload');
|
||
|
|
const statusDiv = document.getElementById('status');
|
||
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
||
|
|
|
||
|
|
if (fileInput.files.length === 0) {
|
||
|
|
statusDiv.innerHTML = '<div class="error">Please select a file to upload</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const file = fileInput.files[0];
|
||
|
|
const formData = new FormData();
|
||
|
|
formData.append('file', file);
|
||
|
|
|
||
|
|
// Show uploading state
|
||
|
|
statusDiv.innerHTML = '<div class="loading">Uploading and analyzing file...</div>';
|
||
|
|
uploadBtn.disabled = true;
|
||
|
|
uploadBtn.textContent = 'Uploading...';
|
||
|
|
|
||
|
|
// Upload file
|
||
|
|
fetch('/upload', {
|
||
|
|
method: 'POST',
|
||
|
|
body: formData
|
||
|
|
})
|
||
|
|
.then(response => response.json())
|
||
|
|
.then(data => {
|
||
|
|
if (data.success) {
|
||
|
|
// File uploaded successfully, now analyze it
|
||
|
|
document.getElementById('filePath').value = data.file_path;
|
||
|
|
statusDiv.innerHTML = '<div class="loading">File uploaded! Analyzing data...</div>';
|
||
|
|
|
||
|
|
// Analyze the uploaded file
|
||
|
|
const sheetFilter = document.getElementById('sheetFilter').value;
|
||
|
|
return fetch('/analyze', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
file_path: data.file_path,
|
||
|
|
sheet_filter: sheetFilter === 'All Sheets' ? null : sheetFilter
|
||
|
|
})
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
throw new Error(data.error);
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.then(response => response.json())
|
||
|
|
.then(data => {
|
||
|
|
if (data.success) {
|
||
|
|
statusDiv.innerHTML = '<div class="success">File uploaded and analyzed successfully!</div>';
|
||
|
|
updateResults(data.results);
|
||
|
|
updateSheetFilter(data.results.sheet_names, data.results.current_sheet_filter);
|
||
|
|
document.getElementById('results').style.display = 'block';
|
||
|
|
} else {
|
||
|
|
statusDiv.innerHTML = `<div class="error">Analysis error: ${data.error}</div>`;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch(error => {
|
||
|
|
statusDiv.innerHTML = `<div class="error">Upload error: ${error.message}</div>`;
|
||
|
|
})
|
||
|
|
.finally(() => {
|
||
|
|
uploadBtn.disabled = false;
|
||
|
|
uploadBtn.textContent = 'Upload & Analyze';
|
||
|
|
handleFileUpload(); // Reset button state based on file selection
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateResults(results) {
|
||
|
|
// Update count displays
|
||
|
|
document.getElementById('matched-count-display').textContent = results.matched_items_count.toLocaleString();
|
||
|
|
|
||
|
|
const totalDifferent = results.mismatches.kst_only_count + results.mismatches.coordi_only_count +
|
||
|
|
results.mismatches.kst_duplicates_count + results.mismatches.coordi_duplicates_count;
|
||
|
|
document.getElementById('different-count-display').textContent = totalDifferent.toLocaleString();
|
||
|
|
|
||
|
|
// Update Summary tab (matched items)
|
||
|
|
updateSummaryTable(results.matched_data);
|
||
|
|
|
||
|
|
// Update Different tab
|
||
|
|
updateDifferentTable(results.mismatch_details);
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSummaryTable(matchedData) {
|
||
|
|
const tbody = document.getElementById('summary-table');
|
||
|
|
tbody.innerHTML = '';
|
||
|
|
|
||
|
|
// Sort by Korean title + episode
|
||
|
|
const sortedData = [...matchedData].sort((a, b) => {
|
||
|
|
const titleCompare = a.title.localeCompare(b.title, 'ko');
|
||
|
|
if (titleCompare !== 0) return titleCompare;
|
||
|
|
|
||
|
|
// Try to sort episodes numerically
|
||
|
|
const aEp = parseFloat(a.episode) || 0;
|
||
|
|
const bEp = parseFloat(b.episode) || 0;
|
||
|
|
return aEp - bEp;
|
||
|
|
});
|
||
|
|
|
||
|
|
sortedData.forEach(item => {
|
||
|
|
const row = tbody.insertRow();
|
||
|
|
row.insertCell(0).textContent = item.title;
|
||
|
|
row.insertCell(1).textContent = item.episode;
|
||
|
|
row.insertCell(2).textContent = item.sheet;
|
||
|
|
row.insertCell(3).textContent = item.row_index ? item.row_index + 1 : item.row;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateDifferentTable(mismatchDetails) {
|
||
|
|
const tbody = document.getElementById('different-table');
|
||
|
|
tbody.innerHTML = '';
|
||
|
|
|
||
|
|
// Combine all mismatches into one array for sorting
|
||
|
|
const allDifferences = [];
|
||
|
|
|
||
|
|
// Add KST-only items
|
||
|
|
mismatchDetails.kst_only.forEach(item => {
|
||
|
|
allDifferences.push({
|
||
|
|
kstData: `${item.title} - Episode ${item.episode}`,
|
||
|
|
coordiData: '',
|
||
|
|
reason: 'Only appears in KST',
|
||
|
|
sortTitle: item.title,
|
||
|
|
sortEpisode: parseFloat(item.episode) || 0
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add Coordi-only items
|
||
|
|
mismatchDetails.coordi_only.forEach(item => {
|
||
|
|
allDifferences.push({
|
||
|
|
kstData: '',
|
||
|
|
coordiData: `${item.title} - Episode ${item.episode}`,
|
||
|
|
reason: 'Only appears in Coordi',
|
||
|
|
sortTitle: item.title,
|
||
|
|
sortEpisode: parseFloat(item.episode) || 0
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add KST duplicates
|
||
|
|
mismatchDetails.kst_duplicates.forEach(item => {
|
||
|
|
allDifferences.push({
|
||
|
|
kstData: `${item.title} - Episode ${item.episode}`,
|
||
|
|
coordiData: '',
|
||
|
|
reason: 'Duplicate in KST',
|
||
|
|
sortTitle: item.title,
|
||
|
|
sortEpisode: parseFloat(item.episode) || 0
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add Coordi duplicates
|
||
|
|
mismatchDetails.coordi_duplicates.forEach(item => {
|
||
|
|
allDifferences.push({
|
||
|
|
kstData: '',
|
||
|
|
coordiData: `${item.title} - Episode ${item.episode}`,
|
||
|
|
reason: 'Duplicate in Coordi',
|
||
|
|
sortTitle: item.title,
|
||
|
|
sortEpisode: parseFloat(item.episode) || 0
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Sort by Korean title + episode
|
||
|
|
allDifferences.sort((a, b) => {
|
||
|
|
const titleCompare = a.sortTitle.localeCompare(b.sortTitle, 'ko');
|
||
|
|
if (titleCompare !== 0) return titleCompare;
|
||
|
|
return a.sortEpisode - b.sortEpisode;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Populate table
|
||
|
|
allDifferences.forEach(diff => {
|
||
|
|
const row = tbody.insertRow();
|
||
|
|
row.insertCell(0).textContent = diff.kstData;
|
||
|
|
row.insertCell(1).textContent = diff.coordiData;
|
||
|
|
row.insertCell(2).textContent = diff.reason;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-analyze on page load with default file
|
||
|
|
window.onload = function() {
|
||
|
|
analyzeData();
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>'''
|
||
|
|
|
||
|
|
html_file = templates_dir / 'index.html'
|
||
|
|
html_file.write_text(html_content)
|
||
|
|
|
||
|
|
def main():
|
||
|
|
# Create templates directory and HTML file
|
||
|
|
create_templates_dir()
|
||
|
|
|
||
|
|
print("Starting web-based GUI...")
|
||
|
|
print("Open your browser and go to: http://localhost:8080")
|
||
|
|
app.run(debug=True, host='0.0.0.0', port=8080)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|