{% include "partials/head.html" %}
<script>
const mediaInfo = JSON.parse("{{ file }}".replaceAll("'", '"').replaceAll(""", "'"));
</script>
<div class="container mt-2" id="page-single">
<h1 class="mb-3">{{ file.name }}</h1>
<p class="text-muted">Path: {{ file.path }}</p>
<div class="d-flex align-items-center gap-2 mb-4">
<a href="/" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> back</a>
<button class="btn btn-outline-secondary ms-auto d-inline-flex align-items-center gap-1" id="btn-delete-file" title="delete file">
<i class="bi bi-trash"></i> delete file
</button>
</div>
<div class="thumbnails-row mb-4" data-file-id="{{ file.id }}">
<div class="thumb-placeholder"></div>
<div class="thumb-placeholder"></div>
<div class="thumb-placeholder"></div>
</div>
<!-- Lightbox -->
<div class="modal fade" id="thumb-lightbox" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-body p-1">
<img src="" class="lightbox-img w-100" style="border-radius: 6px;">
</div>
</div>
</div>
</div>
<!-- Информация о контейнере -->
<div class="card mb-4">
<div class="card-header">
<h2 class="h5 mb-0">// container</h2>
</div>
<div class="card-body">
<p><strong>Bitrate:</strong> {{ file.info.container.bitrate }}</p>
<p><strong>Duration:</strong> <span class="duration-to-minutes" data-duration-in-seconds="{{ file.info.container.duration }}"></span></p>
<p><strong>Size:</strong> {{ file.info.container.size }}</p>
{% include "components/single-transcoding.html" %}
</div>
</div>
<!-- Видео -->
<div class="card mb-4">
<div class="card-header">
<h2 class="h5 mb-0">// video streams</h2>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Codec</th>
<th>Resolution</th>
<th>Bitrate</th>
</tr>
</thead>
<tbody>
{% for video in file.info.video %}
<tr>
<td>{{ video.codec }}</td>
<td>{{ video.resolution }}</td>
<td>{{ video.bitrate }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Аудио -->
<div class="card mb-4">
<div class="card-header">
<h2 class="h5 mb-0">// audio streams</h2>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Title</th>
<th>Bitrate</th>
<th>Channels</th>
<th>Codec</th>
<th>Lang</th>
<th>Layout</th>
<th></th>
</tr>
</thead>
<tbody>
{% for audio in file.info.audio %}
<tr data-track-index="{{ loop.index0 }}"
data-track-codec="{{ audio.codec }}"
data-track-title="{{ audio.title }}"
data-track-language="{{ audio.language }}"
data-track-bitrate="{{ audio.bitrate }}"
data-track-channels="{{ audio.channels }}">
<td>{{ audio.title }}</td>
<td>{{ audio.bitrate }}</td>
<td>{{ audio.channels }}</td>
<td>{{ audio.codec }}</td>
<td>{{ audio.language }}</td>
<td>{{ audio.layout }}</td>
<td class="audio-actions">
<button class="btn btn-outline-secondary btn-sm btn-extract-audio" title="extract">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
<i class="bi bi-box-arrow-up"></i>
</button>
<button class="btn btn-outline-secondary btn-sm btn-remove-audio ms-1" title="remove from video">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Delete file confirmation modal -->
<div class="modal fade" id="confirm-delete-file" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title h6 mb-0">// delete file</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">Delete <strong>{{ file.name }}</strong>?</p>
<small class="text-muted">The file, all its backups, and thumbnail cache will be permanently removed. Extracted audio tracks will not be affected.</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">cancel</button>
<button type="button" class="btn btn-danger btn-sm d-inline-flex align-items-center gap-1" id="confirm-delete-file-btn">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
delete
</button>
</div>
</div>
</div>
</div>
<!-- Delete backup confirmation modal -->
<div class="modal fade" id="confirm-delete-backup" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title h6 mb-0">// delete backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">Delete backup <strong id="confirm-delete-backup-filename"></strong>?</p>
<small class="text-muted">The original file will be permanently removed from disk.</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">cancel</button>
<button type="button" class="btn btn-danger btn-sm d-inline-flex align-items-center gap-1" id="confirm-delete-backup-btn">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
delete
</button>
</div>
</div>
</div>
</div>
<!-- Delete original confirmation modal -->
<div class="modal fade" id="confirm-delete-original" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title h6 mb-0">// transcode without backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">No backup copy will be created. The original <strong class="text-danger">cannot be recovered</strong> after transcoding.</p>
<small class="text-muted">Make sure the transcoded result is acceptable before proceeding.</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">cancel</button>
<button type="button" class="btn btn-danger btn-sm d-inline-flex align-items-center gap-1" id="confirm-run-transcode-btn">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
transcode without backup
</button>
</div>
</div>
</div>
</div>
<!-- Remove audio confirmation modal -->
<div class="modal fade" id="confirm-remove-audio" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title h6 mb-0">// remove audio track</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="mb-1">Remove <strong id="confirm-remove-track-name"></strong> from this video?</p>
<small class="text-muted">The audio stream will be permanently deleted from the file.</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">cancel</button>
<button type="button" class="btn btn-danger btn-sm d-inline-flex align-items-center gap-1" id="confirm-remove-audio-btn">
<span class="spinner-border spinner-border-sm" style="display:none"></span>
remove
</button>
</div>
</div>
</div>
</div>
<!-- Добавить аудиодорожку -->
<div class="card mb-4" id="add-audio-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">// add audio track</h2>
<button class="btn btn-outline-secondary btn-sm" id="toggle-add-audio">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div class="card-body" id="add-audio-body" style="display:none">
<div class="d-flex align-items-center gap-2">
<select class="form-select" id="audio-track-select">
<option value="">loading...</option>
</select>
<button class="btn btn-outline-secondary d-inline-flex align-items-center gap-1" id="btn-add-audio" disabled>
<span class="spinner-border spinner-border-sm" style="display:none"></span>
add
</button>
</div>
</div>
</div>
<!-- Общая информация -->
<div class="card mb-4">
<div class="card-header">
<h2 class="h5 mb-0">// file info</h2>
</div>
<div class="card-body">
<p><strong>Size:</strong> {{ file.size }} {{ file.size_unit }}</p>
<p><strong>Size in Bytes:</strong> {{ file.size_bytes }}</p>
</div>
</div>
{% include "components/file-history.html" %}
</div>
<script src="/static/js/components/thumbnails.js" defer></script>
<script src="/static/js/components/audio-controls.js" defer></script>
<script>
$(document).ready(function() {
const deleteFileModal = new bootstrap.Modal(document.getElementById("confirm-delete-file"));
let deleteFileLocked = false;
$.getJSON("/file-status?path=" + encodeURIComponent(mediaInfo.path), function(data) {
deleteFileLocked = data.transcoding;
});
socket.on("progress", function(data) { if (data.task.file === mediaInfo.path) deleteFileLocked = true; });
socket.on("copy-progress", function(data) { if (data.file === mediaInfo.path) deleteFileLocked = true; });
socket.on("completed", function(data) { if (data.task.file === mediaInfo.path) deleteFileLocked = false; });
socket.on("error", function(data) { if (data.task.file === mediaInfo.path) deleteFileLocked = false; });
socket.on("canceled", function(data) { if (data.task.file === mediaInfo.path) deleteFileLocked = false; });
$("#btn-delete-file").on("click", function() {
if (deleteFileLocked) {
pushInfoMsg("Cannot delete file while transcoding is in progress.");
return;
}
deleteFileModal.show();
});
$("#confirm-delete-file-btn").on("click", function() {
const btn = $(this);
btn.prop("disabled", true).find(".spinner-border").show();
$.ajax({
url: "/delete-file",
method: "POST",
contentType: "application/json",
data: JSON.stringify({ path: mediaInfo.path }),
success: function() {
window.location.href = "/";
},
error: function(xhr) {
btn.prop("disabled", false).find(".spinner-border").hide();
deleteFileModal.hide();
pushErrMsg("Delete failed: " + (xhr.responseJSON?.error || "unknown error"));
}
});
});
});
</script>
{% include "partials/footer.html" %}