mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-14 08:06:47 +02:00
503 lines
22 KiB
Twig
503 lines
22 KiB
Twig
{% extends 'admin/layout.twig' %}
|
|
|
|
{% block title %}Sync Media - Admin{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 class="h3 mb-0">Sync Media</h1>
|
|
<div>
|
|
<button type="button" class="btn btn-primary" id="runFullSync">
|
|
<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">Sync Status</h6>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="syncOptions" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-gear me-1"></i>Options
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="syncOptions">
|
|
<li><a class="dropdown-item" href="#" id="clearCompleted">Clear Completed</a></li>
|
|
<li><a class="dropdown-item" href="#" id="clearAll">Clear All</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#" id="refreshLogs">Refresh</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="syncLogs" class="log-container">
|
|
<div class="text-center text-muted py-4">
|
|
<i class="bi bi-arrow-repeat display-4"></i>
|
|
<p class="mt-2">No sync activity yet</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Sync Statistics</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center mb-4">
|
|
<div class="position-relative d-inline-block">
|
|
<div id="syncProgress" class="progress-circle" data-value="0">
|
|
<span class="progress-left">
|
|
<span class="progress-bar"></span>
|
|
</span>
|
|
<span class="progress-right">
|
|
<span class="progress-bar"></span>
|
|
</span>
|
|
<div class="progress-value w-100 h-100 rounded-circle d-flex align-items-center justify-content-center">
|
|
<div class="h2 font-weight-bold">0<small>%</small></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<h5 class="mt-3" id="syncStatus">Idle</h5>
|
|
<p class="text-muted mb-0">Last sync: <span id="lastSyncTime">Never</span></p>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>Movies</span>
|
|
<span id="movieCount">0</span>
|
|
</div>
|
|
<div class="progress" style="height: 5px;">
|
|
<div id="movieProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>TV Shows</span>
|
|
<span id="tvShowCount">0</span>
|
|
</div>
|
|
<div class="progress" style="height: 5px;">
|
|
<div id="tvShowProgress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>Music</span>
|
|
<span id="musicCount">0</span>
|
|
</div>
|
|
<div class="progress" style="height: 5px;">
|
|
<div id="musicProgress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>Adult Videos</span>
|
|
<span id="adultVideoCount">0</span>
|
|
</div>
|
|
<div class="progress" style="height: 5px;">
|
|
<div id="adultVideoProgress" class="progress-bar bg-danger" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Quick Actions</h6>
|
|
</div>
|
|
<div class="list-group list-group-flush">
|
|
<button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" id="scanLibraries">
|
|
<span><i class="bi bi-search me-2"></i> Scan Libraries</span>
|
|
<span class="badge bg-primary rounded-pill" id="pendingScans">0</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action" id="syncStashPerformers">
|
|
<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers
|
|
</button>
|
|
<button class="list-group-item list-group-item-action">
|
|
<i class="bi bi-arrow-clockwise me-2"></i> Update Metadata
|
|
</button>
|
|
<button class="list-group-item list-group-item-action">
|
|
<i class="bi bi-images me-2"></i> Refresh Images
|
|
</button>
|
|
<button class="list-group-item list-group-item-action text-danger" id="cancelSync">
|
|
<i class="bi bi-x-circle me-2"></i> Cancel Sync
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block styles %}
|
|
<style>
|
|
.log-container {
|
|
height: 400px;
|
|
overflow-y: auto;
|
|
background-color: #1e1e1e;
|
|
color: #e0e0e0;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
padding: 3px 0;
|
|
border-bottom: 1px solid #2d2d2d;
|
|
}
|
|
|
|
.log-entry:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.log-time {
|
|
color: #6c757d;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.log-info {
|
|
color: #4e73df;
|
|
}
|
|
|
|
.log-success {
|
|
color: #1cc88a;
|
|
}
|
|
|
|
.log-warning {
|
|
color: #f6c23e;
|
|
}
|
|
|
|
.log-error {
|
|
color: #e74a3b;
|
|
}
|
|
|
|
/* Circular progress */
|
|
.progress-circle {
|
|
width: 150px;
|
|
height: 150px;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-circle .progress-left,
|
|
.progress-circle .progress-right {
|
|
width: 150px;
|
|
height: 150px;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
border: 10px solid #f8f9fc;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.progress-circle .progress-bar {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: transparent;
|
|
border-width: 10px;
|
|
border-style: solid;
|
|
position: absolute;
|
|
top: -10px;
|
|
left: -10px;
|
|
}
|
|
|
|
.progress-circle .progress-left .progress-bar {
|
|
left: 100%;
|
|
border-top-right-radius: 80px;
|
|
border-bottom-right-radius: 80px;
|
|
border-left: 0;
|
|
transform-origin: center left;
|
|
}
|
|
|
|
.progress-circle .progress-right .progress-bar {
|
|
left: -100%;
|
|
border-top-left-radius: 80px;
|
|
border-bottom-left-radius: 80px;
|
|
border-right: 0;
|
|
transform-origin: center right;
|
|
}
|
|
|
|
.progress-circle .progress-value {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
right: 10px;
|
|
bottom: 10px;
|
|
background-color: #fff;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 0.15rem 1rem rgba(0, 0, 0, 0.1);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize tooltips
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
|
|
// Handle run full sync
|
|
$('#runFullSync').click(function() {
|
|
if (confirm('Are you sure you want to run a full sync? This may take a while.')) {
|
|
startSync('full');
|
|
}
|
|
});
|
|
|
|
// Handle scan libraries
|
|
$('#scanLibraries').click(function() {
|
|
startSync('scan');
|
|
});
|
|
|
|
// Handle sync Stash performers
|
|
$('#syncStashPerformers').click(function() {
|
|
if (confirm('Are you sure you want to sync existing performers with Stash? This will update your local performer data with information from Stash.')) {
|
|
syncStashPerformers();
|
|
}
|
|
});
|
|
|
|
// Handle cancel sync
|
|
$('#cancelSync').click(function() {
|
|
if (confirm('Are you sure you want to cancel the current sync?')) {
|
|
cancelSync();
|
|
}
|
|
});
|
|
|
|
// Handle clear logs
|
|
$('#clearCompleted').click(function(e) {
|
|
e.preventDefault();
|
|
clearLogs('completed');
|
|
});
|
|
|
|
$('#clearAll').click(function(e) {
|
|
e.preventDefault();
|
|
clearLogs('all');
|
|
});
|
|
|
|
// Handle refresh logs
|
|
$('#refreshLogs').click(function(e) {
|
|
e.preventDefault();
|
|
loadSyncStatus();
|
|
});
|
|
|
|
// Function to start sync
|
|
function startSync(type) {
|
|
// Show loading state
|
|
$('#runFullSync').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
|
|
|
// Make AJAX request to start sync
|
|
$.post('{{ path_for("admin.sync.start") }}', { type: type }, function(response) {
|
|
if (response.success) {
|
|
addLog('Sync started successfully', 'success');
|
|
// Start polling for updates
|
|
pollSyncStatus();
|
|
} else {
|
|
addLog('Error starting sync: ' + (response.message || 'Unknown error'), 'error');
|
|
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
|
}
|
|
}).fail(function() {
|
|
addLog('Failed to start sync. Please try again.', 'error');
|
|
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
|
});
|
|
}
|
|
|
|
// Function to sync Stash performers
|
|
function syncStashPerformers() {
|
|
// Show loading state
|
|
$('#syncStashPerformers').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
|
|
|
// Make AJAX request to sync performers
|
|
$.post('/api/actors/sync-existing-stash', function(response) {
|
|
if (response.success) {
|
|
addLog('Stash performer sync completed successfully', 'success');
|
|
addLog('Processed: ' + response.results.processed + ', Updated: ' + response.results.updated + ', Skipped: ' + response.results.skipped, 'info');
|
|
|
|
if (response.results.not_found_in_stash && response.results.not_found_in_stash.length > 0) {
|
|
addLog('Found ' + response.results.not_found_in_stash.length + ' performers not in Stash - check reports', 'warning');
|
|
}
|
|
|
|
if (response.results.errors && response.results.errors.length > 0) {
|
|
addLog('Encountered ' + response.results.errors.length + ' errors during sync', 'error');
|
|
}
|
|
} else {
|
|
addLog('Error syncing performers: ' + (response.error || 'Unknown error'), 'error');
|
|
}
|
|
|
|
$('#syncStashPerformers').prop('disabled', false).html('<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers');
|
|
}).fail(function(xhr, status, error) {
|
|
addLog('Failed to sync performers: ' + error, 'error');
|
|
$('#syncStashPerformers').prop('disabled', false).html('<i class="bi bi-person-lines-fill me-2"></i> Sync Stash Performers');
|
|
});
|
|
}
|
|
|
|
// Function to cancel sync
|
|
function cancelSync() {
|
|
$.post('{{ path_for("admin.sync.cancel") }}', function(response) {
|
|
if (response.success) {
|
|
addLog('Sync cancelled', 'warning');
|
|
} else {
|
|
addLog('Error cancelling sync: ' + (response.message || 'Unknown error'), 'error');
|
|
}
|
|
}).fail(function() {
|
|
addLog('Failed to cancel sync. Please try again.', 'error');
|
|
});
|
|
}
|
|
|
|
// Function to clear logs
|
|
function clearLogs(type) {
|
|
// Make AJAX request to clear logs
|
|
$.post('{{ path_for("admin.sync.clear-logs") }}', { type: type }, function(response) {
|
|
if (response.success) {
|
|
if (type === 'all') {
|
|
$('#syncLogs').html('<div class="text-center text-muted py-4"><i class="bi bi-arrow-repeat display-4"></i><p class="mt-2">No sync activity yet</p></div>');
|
|
} else {
|
|
$('.log-entry.completed').remove();
|
|
if ($('.log-entry').length === 0) {
|
|
$('#syncLogs').html('<div class="text-center text-muted py-4"><i class="bi bi-arrow-repeat display-4"></i><p class="mt-2">No sync activity yet</p></div>');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to add log entry
|
|
function addLog(message, type = 'info') {
|
|
let logContainer = $('#syncLogs');
|
|
const now = new Date();
|
|
const timeString = now.toLocaleTimeString();
|
|
|
|
// Remove empty state if present
|
|
if (logContainer.find('.text-center').length) {
|
|
logContainer.empty();
|
|
}
|
|
|
|
// Create log entry
|
|
const logEntry = $(`
|
|
<div class="log-entry">
|
|
<span class="log-time">[${timeString}]</span>
|
|
<span class="log-${type}">${message}</span>
|
|
</div>
|
|
`);
|
|
|
|
// Add to container and scroll to bottom
|
|
logContainer.append(logEntry);
|
|
logContainer.scrollTop(logContainer[0].scrollHeight);
|
|
}
|
|
|
|
// Function to update progress
|
|
function updateProgress(progress) {
|
|
// Update circular progress
|
|
const progressValue = progress.percent || 0;
|
|
const $progressCircle = $('.progress-circle');
|
|
|
|
// Animate progress circle
|
|
if (progressValue > 0) {
|
|
const $leftCircle = $progressCircle.find('.progress-left .progress-bar');
|
|
const $rightCircle = $progressCircle.find('.progress-right .progress-bar');
|
|
|
|
if (progressValue <= 50) {
|
|
$rightCircle.css('transform', `rotate(${progressValue * 3.6}deg)`);
|
|
$leftCircle.css('transform', 'rotate(0deg)');
|
|
} else {
|
|
$rightCircle.css('transform', 'rotate(180deg)');
|
|
$leftCircle.css('transform', `rotate(${(progressValue - 50) * 3.6}deg)`);
|
|
}
|
|
|
|
$progressCircle.find('.progress-value div').text(`${Math.round(progressValue)}%`);
|
|
}
|
|
|
|
// Update status text
|
|
if (progress.status) {
|
|
$('#syncStatus').text(progress.status);
|
|
}
|
|
|
|
// Update last sync time
|
|
if (progress.lastSync) {
|
|
$('#lastSyncTime').text(progress.lastSync);
|
|
}
|
|
|
|
// Update media type counts and progress
|
|
if (progress.movies) {
|
|
$('#movieCount').text(progress.movies.processed + ' / ' + progress.movies.total);
|
|
const moviePercent = progress.movies.total > 0 ? Math.round((progress.movies.processed / progress.movies.total) * 100) : 0;
|
|
$('#movieProgress').css('width', moviePercent + '%');
|
|
}
|
|
|
|
if (progress.tvShows) {
|
|
$('#tvShowCount').text(progress.tvShows.processed + ' / ' + progress.tvShows.total);
|
|
const tvShowPercent = progress.tvShows.total > 0 ? Math.round((progress.tvShows.processed / progress.tvShows.total) * 100) : 0;
|
|
$('#tvShowProgress').css('width', tvShowPercent + '%');
|
|
}
|
|
|
|
if (progress.music) {
|
|
$('#musicCount').text(progress.music.processed + ' / ' + progress.music.total);
|
|
const musicPercent = progress.music.total > 0 ? Math.round((progress.music.processed / progress.music.total) * 100) : 0;
|
|
$('#musicProgress').css('width', musicPercent + '%');
|
|
}
|
|
|
|
// Update sync button state
|
|
if (progress.isRunning) {
|
|
$('#runFullSync').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
|
$('#cancelSync').prop('disabled', false);
|
|
} else {
|
|
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
|
$('#cancelSync').prop('disabled', true);
|
|
}
|
|
}
|
|
|
|
// Function to poll for sync status
|
|
function pollSyncStatus() {
|
|
loadSyncStatus();
|
|
|
|
// Only continue polling if sync is in progress
|
|
if ($('#syncStatus').text() === 'In Progress') {
|
|
setTimeout(pollSyncStatus, 2000);
|
|
}
|
|
}
|
|
|
|
// Function to load sync status
|
|
function loadSyncStatus() {
|
|
$.get('{{ path_for("admin.sync.status") }}', function(response) {
|
|
if (response.success) {
|
|
updateProgress(response.progress);
|
|
|
|
// Update logs if available
|
|
if (response.logs && response.logs.length > 0) {
|
|
const $logContainer = $('#syncLogs');
|
|
$logContainer.empty();
|
|
|
|
response.logs.forEach(log => {
|
|
const logEntry = $(`
|
|
<div class="log-entry ${log.status === 'completed' ? 'completed' : ''}">
|
|
<span class="log-time">[${log.time}]</span>
|
|
<span class="log-${log.type}">${log.message}</span>
|
|
</div>
|
|
`);
|
|
$logContainer.append(logEntry);
|
|
});
|
|
|
|
$logContainer.scrollTop($logContainer[0].scrollHeight);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initial load
|
|
loadSyncStatus();
|
|
});
|
|
</script>
|
|
{% endblock %}
|