2025-04-18 15:36:04 +09:00

409 lines
18 KiB
HTML

{% extends "layout.html" %}
{% block title %}
Dashcam Segments
{% endblock %}
{% block main %}
{% autoescape false %}
<br>
<h1>Dashcam Segments (one per minute)</h1>
<br>
<video id="video" width="640" height="480" controls autoplay style="background:black">
</video>
<br><br>
<div class="camera-switcher d-flex justify-content-center gap-2 mb-3">
<a href="{{ route }}?{{ query_segment }},qcamera"
class="btn btn-sm {% if query_type == 'qcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">qcamera</a>
<a href="{{ route }}?{{ query_segment }},fcamera"
class="btn btn-sm {% if query_type == 'fcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">fcamera</a>
<a href="{{ route }}?{{ query_segment }},dcamera"
class="btn btn-sm {% if query_type == 'dcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">dcamera</a>
<a href="{{ route }}?{{ query_segment }},ecamera"
class="btn btn-sm {% if query_type == 'ecamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">ecamera</a>
</div>
<div class="video-info">
current segment: <span id="currentsegment" class="badge bg-primary"></span>
<br>
current view: <span id="currentview" class="badge bg-secondary"></span>
<br>
date: <span id="folderDate" class="badge bg-info"></span>
</div>
<br>
<div class="download-section text-center">
<div class="d-flex flex-column align-items-center gap-3">
<div>
<div class="d-flex align-items-center gap-2">
<span>Full Downloads:</span>
<div class="d-flex flex-wrap gap-2">
<a download="{{ route }}-qcamera.mp4" href="/footage/full/qcamera/{{ route }}" class="btn btn-sm btn-outline-primary">qcamera</a>
<a download="{{ route }}-fcamera.mp4" href="/footage/full/fcamera/{{ route }}" class="btn btn-sm btn-outline-primary">fcamera</a>
<a download="{{ route }}-dcamera.mp4" href="/footage/full/dcamera/{{ route }}" class="btn btn-sm btn-outline-primary">dcamera</a>
<a download="{{ route }}-ecamera.mp4" href="/footage/full/ecamera/{{ route }}" class="btn btn-sm btn-outline-primary">ecamera</a>
</div>
</div>
</div>
<div>
<div class="d-flex align-items-center gap-2">
<span>Segment Downloads:</span>
<div class="d-flex flex-wrap gap-2">
<a download="{{ route }}-rlog-{{query_segment}}.mp4" href="/footage/full/rlog/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">rlog</a>
<a download="{{ route }}-qcamera-{{query_segment}}.mp4" href="/footage/full/qcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">qcamera</a>
<a download="{{ route }}-fcamera-{{query_segment}}.mp4" href="/footage/full/fcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">fcamera</a>
<a download="{{ route }}-dcamera-{{query_segment}}.mp4" href="/footage/full/dcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">dcamera</a>
<a download="{{ route }}-ecamera-{{query_segment}}.mp4" href="/footage/full/ecamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">ecamera</a>
</div>
</div>
</div>
</div>
</div>
<div class="segment-controls mt-4">
<h3>Segment List</h3>
<div class="d-flex align-items-center mb-3">
<button onclick="toggleAllSegments()" class="btn btn-sm btn-outline-secondary me-2">Select All</button>
<div id="selectedCount" class="text-muted">0 segments selected</div>
</div>
<div class="segment-list" style="max-height: 400px; overflow-y: auto;">
{% for segment in segments.split(',') %}
{% set clean_segment = segment.strip().strip("'") %}
{% if clean_segment %}
{% set seg_num = clean_segment.split('--')[2] %}
<div class="segment-item d-flex align-items-start py-2 border-bottom ps-2">
<input type="checkbox"
name="selected_segments"
value="{{ clean_segment }}"
class="form-check-input me-3 segment-checkbox mt-1"
style="transform: scale(1.5)">
<div class="d-flex flex-column">
<a href="{{ route }}?{{ seg_num }},{{ query_type }}" class="text-decoration-none">
{{ clean_segment }}
</a>
<small class="text-muted" id="segment-info-{{ seg_num }}">
<i class="bi bi-clock-history me-1"></i><span id="segment-date-{{ seg_num }}">Loading date...</span>
<span class="mx-2"></span>
<i class="bi bi-hdd me-1"></i><span id="segment-size-{{ seg_num }}">Loading size...</span>
</small>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="mt-4">
<button class="btn btn-danger" onclick="uploadSelectedSegments()">
<i class="bi bi-upload me-2"></i>
Upload All Selected Routes To Carrot Server
</button>
<div id="uploadStatus" class="mt-2"></div>
</div>
</div>
<script>
const segments = [{{ segments }}];
const currentSegment = "{{ route }}--{{ query_segment }}";
let currentSegmentIndex = segments.findIndex(seg => seg === currentSegment);
if (currentSegmentIndex === -1) currentSegmentIndex = 0;
const video = document.getElementById('video');
video.src = `/footage/{{ query_type }}/${currentSegment}`;
document.getElementById("currentsegment").textContent = currentSegment;
document.getElementById("currentview").textContent = "{{ query_type }}";
video.load();
video.play().catch(e => console.log("Autoplay prevented:", e));
video.addEventListener('ended', function() {
currentSegmentIndex = (currentSegmentIndex + 1) % segments.length;
const nextSegment = segments[currentSegmentIndex];
const segNum = nextSegment.split('--')[2];
window.location.href = `{{ route }}?${segNum},{{ query_type }}`;
});
function toggleAllSegments() {
const checkboxes = document.querySelectorAll('.segment-checkbox');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
updateSelectionCount();
}
function updateSelectionCount() {
const count = document.querySelectorAll('.segment-checkbox:checked').length;
document.getElementById('selectedCount').textContent = `${count} segments selected`;
}
async function getFolderInfo(folderPath) {
try {
const response = await fetch(`/folder-info?path=${encodeURIComponent(folderPath)}`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (e) {
console.error(`Error getting folder info for ${folderPath}:`, e);
return null;
}
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
async function updateCurrentFolderInfo() {
const folderPath = `/data/media/0/realdata/${currentSegment}`;
const info = await getFolderInfo(folderPath);
const dateElement = document.getElementById('folderDate');
if (info && info.created_date) {
dateElement.textContent = info.created_date;
if (info.size) {
dateElement.textContent += `${formatBytes(info.size)}`;
}
} else {
dateElement.textContent = "정보 없음";
}
}
async function updateSegmentListInfo() {
for (const segment of segments) {
const cleanSegment = segment.trim().replace(/'/g, '');
if (!cleanSegment) continue;
const segNum = cleanSegment.split('--')[2];
const folderPath = `/data/media/0/realdata/${cleanSegment}`;
const info = await getFolderInfo(folderPath);
const dateElement = document.getElementById(`segment-date-${segNum}`);
const sizeElement = document.getElementById(`segment-size-${segNum}`);
if (info) {
if (info.created_date) {
dateElement.textContent = info.created_date;
} else {
dateElement.textContent = "날짜 없음";
}
if (info.size) {
sizeElement.textContent = formatBytes(info.size);
} else {
sizeElement.textContent = "크기 없음";
}
} else {
dateElement.textContent = "정보 없음";
sizeElement.textContent = "정보 없음";
}
}
}
async function uploadSelectedSegments() {
const video = document.getElementById('video');
video.pause();
const selected = Array.from(document.querySelectorAll('.segment-checkbox:checked'))
.map(cb => cb.value);
if(selected.length === 0) {
showUploadStatus('No segments selected!', 'danger');
return;
}
const progressUI = `
<div class="upload-progress mt-3">
<div class="progress mb-2">
<div id="uploadProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<div class="row">
<div class="col-6 text-start">
<small id="uploadProgressText">Preparing upload...</small>
</div>
<div class="col-6 text-end">
<small id="uploadSpeed">-</small>
</div>
</div>
<div class="row mt-1">
<div class="col-12">
<small id="uploadFileInfo" class="text-muted">-</small>
</div>
</div>
</div>
`;
const statusDiv = document.getElementById('uploadStatus');
statusDiv.innerHTML = progressUI;
try {
let totalSize = 0;
let uploadedSize = 0;
const startTime = Date.now();
const uploads = [];
const selectedSegmentsCount = selected.length;
document.getElementById('uploadProgressText').textContent = 'Calculating total size...';
for (const segment of selected) {
const segmentNum = segment.split('--')[2];
const files = ['rlog.zst', 'qcamera.ts'];
for (const file of files) {
try {
const response = await fetch(`/file-size?path=/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`);
const { size } = await response.json();
if (size) {
totalSize += size;
uploads.push({ segmentNum, file, size });
}
} catch (e) {
console.warn(`Size check failed for ${file}`, e);
}
}
}
const initialUploadMsg = `Uploading ${uploads.length} files (${selectedSegmentsCount} Segments) ${formatBytes(totalSize)}...`;
showUploadStatus(initialUploadMsg, 'info');
document.getElementById('uploadProgressText').textContent = initialUploadMsg;
document.getElementById('uploadFileInfo').textContent =
`${selectedSegmentsCount} Segments | ${uploads.length} files | ${formatBytes(totalSize)}`;
for (const { segmentNum, file, size } of uploads) {
try {
const filePath = `/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`;
document.getElementById('uploadFileInfo').textContent =
`Uploading: ${file} (${formatBytes(size)})`;
const formData = new FormData();
const blob = await fetch(filePath).then(r => r.blob());
formData.append('file', blob, file);
formData.append('segment', segmentNum);
const xhr = new XMLHttpRequest();
xhr.open('POST', `/footage/full/upload_carrot/{{ route }}/${segmentNum}`, true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable && totalSize > 0) {
const loaded = uploadedSize + e.loaded;
const percent = Math.round(loaded / totalSize * 100);
const elapsed = (Date.now() - startTime) / 1000;
const speed = elapsed > 0 ? loaded / (1024 * 1024 * elapsed) : 0;
document.getElementById('uploadProgressBar').style.width = `${percent}%`;
document.getElementById('uploadProgressText').textContent =
`${percent}% (${formatBytes(loaded)}/${formatBytes(totalSize)})`;
document.getElementById('uploadSpeed').textContent =
`${speed.toFixed(2)} MB/s`;
}
};
await new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status === 200) {
uploadedSize += size;
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.send(formData);
});
} catch (error) {
console.error(`Upload error for ${file}:`, error);
showUploadStatus(`Error: ${error.message}`, 'danger');
throw error;
}
}
const successAlert = document.createElement('div');
successAlert.className = 'alert alert-success';
successAlert.textContent = `Upload complete! ${selectedSegmentsCount} segments uploaded`;
statusDiv.prepend(successAlert);
const initialAlert = statusDiv.querySelector('.alert-info');
if (initialAlert) initialAlert.remove();
const progressElements = document.querySelectorAll('.upload-progress, #uploadProgressBar, #uploadProgressText, #uploadSpeed, #uploadFileInfo');
progressElements.forEach(el => el.remove());
} catch (error) {
console.error('Upload failed:', error);
showUploadStatus(`Upload failed: ${error.message}`, 'danger');
document.getElementById('uploadProgressBar').classList.remove('progress-bar-animated');
document.getElementById('uploadProgressBar').classList.add('bg-danger');
}
}
function showUploadStatus(message, type, append = false) {
const statusDiv = document.getElementById('uploadStatus');
if (!append) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} mb-2`;
alertDiv.textContent = message;
statusDiv.prepend(alertDiv);
}
}
document.addEventListener('DOMContentLoaded', function() {
updateSelectionCount();
updateCurrentFolderInfo();
updateSegmentListInfo();
});
</script>
{% endautoescape %}
<br><br>
{% endblock %}
<style>
.upload-progress {
background: #f8f9fa;
border-radius: 8px;
padding: 10px;
margin-top: 10px;
}
.progress {
height: 20px;
}
.progress-bar {
transition: width 0.3s ease;
}
.download-section .btn-group .btn {
border-radius: 20px !important;
margin: 0 2px;
}
.download-section span.me-2 {
font-size: 0.9em;
color: #666;
}
.download-section .btn {
min-width: 90px;
padding: 0.25rem 0.5rem;
}
.download-section .d-flex.align-items-center {
flex-wrap: wrap;
row-gap: 8px;
}
.camera-switcher .btn {
min-width: 80px;
transition: all 0.2s ease;
}
.segment-item {
align-items: flex-start !important;
}
.segment-item small {
font-size: 0.8rem;
margin-top: 2px;
}
.segment-item .form-check-input {
margin-top: 0.3rem;
}
.badge {
font-weight: 500;
}
#folderDate {
font-size: 0.9em;
}
</style>