Add Show Contact Sheet to server WebUI

This commit is contained in:
Jafea7 2025-09-30 13:55:27 +10:00
parent 4ad22bb46a
commit 41a0553c45
2 changed files with 172 additions and 21 deletions

View File

@ -80,6 +80,46 @@
<div id="alert-container"></div>
<!-- Contact sheet modal -->
<div class="modal fade" id="contactsheetModal" tabindex="-1" aria-labelledby="contactsheetModalLabel" aria-hidden="true">
<div class="modal-dialog" style="max-width: 80vw;">
<div class="modal-content modal-content-contactsheet">
<div class="modal-header">
<h6 class="modal-title title-truncate" id="contactsheetModalLabel">Image Title</h6>
<button type="button" class="btn btn-danger fa fa-times-circle" data-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img id="imageModalImage" src="" alt="Contact Sheet" class="img-fluid" style="max-width: 100%; height: auto;" />
</div>
<div class="modal-footer">
<button id="deleteRecordingBtn" type="button" class="btn btn-danger"><i class="fa fa-trash"></i></button>
</div>
</div>
</div>
</div>
<!-- <div class="modal fade" id="contactsheetModal" tabindex="-1" aria-labelledby="contactsheetModalLabel" aria-hidden="true">
<div class="modal-dialog" style="max-width: 80vw;">
<div class="modal-content modal-content-contactsheet">
<div class="modal-header">
<h6 class="modal-title title-truncate" id="contactsheetModalLabel">Image Title</h6>
<button type="button" class="btn btn-danger fas fa-times-circle" data-dismiss="modal" aria-label="Close">
<span class="sr-only">Close</span>
</button>
</div>
<div class="modal-body text-center">
<img id="imageModalImage" src="placeholder.jpg" alt="Contact Sheet" class="img-fluid" style="max-width: 100%; height: auto;" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger fas fa-trash"
data-bind="enable: (ko_status() === 'FINISHED' && ko_contactSheet() !== null), click: show, attr: { title: (ko_status() === 'FINISHED' && ko_contactSheet() !== null) ? 'Show contact sheet' : 'No contact sheet' }">
<span class="sr-only">Show contact sheet</span>
</button>
</div>
</div>
</div>
</div> -->
<div class="tab-content" id="myTabContent">
<section id="models" class="tab-pane fade active show" role="tabpanel" aria-labelledby="models-tab">
<div class="container">
@ -182,12 +222,12 @@
<th><a href="#" data-bind="orderable: {collection: 'recordings', field: 'model.name'}">Model</a></th>
<th><a href="#" data-bind="orderable: {collection: 'recordings', field: 'startDate'}">Date</a></th>
<th><a href="#" data-bind="orderable: {collection: 'recordings', field: 'ko_status'}">Status</a></th>
<!-- <th><a href="#" data-bind="orderable: {collection: 'recordings', field: 'progress'}">Progress</a></th> -->
<th><a href="#" data-bind="orderable: {collection: 'recordings', field: 'sizeInByte'}">Size</a></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: recordings">
@ -195,8 +235,10 @@
<td><a data-bind="attr: { href: model.url, title: model.name }, text: model.name"></a></td>
<td data-bind="text: ko_date"></td>
<td data-bind="text: ko_status"></td>
<!-- <td data-bind="text: ko_progressString"></td> -->
<td data-bind="text: ko_size"></td>
<td>
<button class="btn btn-secondary fa fa-image" data-bind="enable: (ko_status() == 'FINISHED' && ko_contactSheet() !== null), click: show, attr: { title: (ko_status() == 'FINISHED' && ko_contactSheet() !== null) ? 'Show contact sheet' : 'No contact sheet' }"></button>
</td>
<td>
<button class="btn btn-secondary fa fa-play" title="Play recording" data-bind="click: play"></button>
</td>
@ -207,12 +249,12 @@
<td>
<button class="btn btn-secondary fa fa-recycle" title="Rerun processing"
data-bind="enable: (ko_status() == 'WAITING' || ko_status() == 'FAILED' || ko_status() == 'FINISHED'),
click: ctbrec.rerunProcessing"></button>
click: rerunProcessing"></button>
</td>
<td>
<button class="btn btn-secondary fa fa-trash" title="Delete recording"
data-bind="enable: (ko_status() == 'FINISHED' || ko_status() == 'WAITING' || ko_status() == 'FAILED'),
click: ctbrec.deleteRecording"></button>
click: deleteRecording"></button>
</td>
</tr>
</tbody>
@ -297,9 +339,10 @@
<script src="vendor/CryptoJS/hmac-sha256.js"></script>
<!-- ctbrec stuff -->
<script src="config.js"></script>
<script src="recordings.js"></script>
<script src="models.js"></script>
<script src="config.js"></script>
<script>
let cookieJar = cookie.cookie;
cookieJar.defaults.expires = 365 * 100;
@ -450,17 +493,17 @@
});
}
function addModelKeyPressed(e) {
let charCode = (typeof e.which === "number") ? e.which : e.keyCode;
if (charCode === 13) {
addModel();
} else {
$('#addModelByUrl').autocomplete({
source: ["AmateurTv:", "BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "CherryTv:", "Dreamcam:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MVLive:", "MyFreeCams:", "SecretFriends:", "Showup:", "Streamate:", "Streamray:", "Stripchat:", "XloveCam:"]
});
}
}
function addModelKeyPressed(e) {
let charCode = (typeof e.which === "number") ? e.which : e.keyCode;
if (charCode === 13) {
addModel();
} else {
$('#addModelByUrl').autocomplete({
source: ["AmateurTv:", "BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "CherryTv:", "Dreamcam:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MVLive:", "MyFreeCams:", "SecretFriends:", "Showup:", "Streamate:", "Streamray:", "Stripchat:", "XloveCam:"]
});
}
}
let ctbrec = {
add: function(input, duration, onsuccess) {
@ -588,7 +631,7 @@
}
},
rerunProcessing: function(recording) {
/* rerunProcessing: function(recording) {
let name = recording.model.name + ' ' + recording.ko_date();
try {
let action = '{"action": "rerunPostProcessing", "recording": ' + JSON.stringify(recording) + '}';
@ -634,7 +677,7 @@
} catch (e) {
if (console) console.log('Unexpected error', e);
}
}
} */
};
$(document).ready(function() {
@ -682,6 +725,11 @@
throughput
});
// Required for contact sheets
window.onload = function() {
loadConfig();
};
updateOnlineModels();
updateRecordings();

View File

@ -58,6 +58,54 @@ function download(recording) {
location.href = src;
}
function rerunProcessing(recording) {
let name = recording.model.name + ' ' + recording.ko_date();
try {
let action = '{"action": "rerunPostProcessing", "recording": ' + JSON.stringify(recording) + '}';
$.ajax({
type: 'POST',
url: '../rec',
dataType: 'json',
async: true,
timeout: 60000,
headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)},
data: action
});
} catch (e) {
if (console) console.log('Unexpected error', e);
}
}
function deleteRecording(recording) {
let name = recording.model.name + ' ' + recording.ko_date();
try {
let action = '{"action": "delete", "recording": ' + JSON.stringify(recording) + '}';
$.ajax({
type: 'POST',
url: '../rec',
dataType: 'json',
async: true,
timeout: 60000,
headers: {'CTBREC-HMAC': CryptoJS.HmacSHA256(action, hmac)},
data: action
})
.done(function(data) {
if (data.status === 'success') {
$.notify('Removed recording ' + name, 'info');
observableRecordingsArray.remove(recording);
} else {
$.notify('Removing recording ' + name + ' failed', 'error');
}
})
.fail(function(jqXHR, textStatus, errorThrown) {
if (console) console.log(textStatus, errorThrown);
$.notify('Removing recording ' + name + ' failed', 'error');
});
} catch (e) {
if (console) console.log('Unexpected error', e);
}
}
function calculateSize(sizeInByte) {
let size = sizeInByte;
let unit = "Bytes";
@ -84,6 +132,32 @@ function isRecordingInArray(array, recording) {
return false;
}
function show(recording) {
if (console) console.log("Show: " + recording.ko_contactSheet());
let localFilePath = recording.ko_contactSheet();
let src = getImageUrl(recording.ko_contactSheet());
if (console) console.log("Show: " + src);
if (src) {
if (console) console.log("Image: " + src);
// Update the modal's image source and display the modal
let modalImage = document.getElementById('imageModalImage');
modalImage.src = src;
let fileName = localFilePath.split('\\').pop().split('/').pop();
let modalTitle = document.getElementById('contactsheetModalLabel');
modalTitle.innerText = fileName; // Set the title to the file name
// Show the modal (assuming you are using Bootstrap's modal)
$('#contactsheetModal').modal('show');
// Update the delete button to use this recording
document.getElementById('deleteRecordingBtn').onclick = function() {
$('#contactsheetModal').modal('hide');
deleteRecording(recording);
};
} else {
console.log('No contact sheet available');
}
document.activeElement.blur();
}
/**
* Synchronizes recordings from the server with the displayed knockout recordings table
*/
@ -112,9 +186,11 @@ function syncRecordings(recordings) {
'second' : '2-digit'
})
});
recording.ko_progressString = ko.observable(recording.progress === -1 ? '' : recording.progress);
recording.ko_size = ko.observable(calculateSize(recording.sizeInByte));
recording.ko_status = ko.observable(recording.status);
recording.ko_contactSheet = ko.observable(
recording.associatedFiles?.find(file => file.endsWith('.jpg') || file.endsWith('.png')) || null
);
if (recording.singleFile) {
recording.playlist = '/hls/' + recording.id;
} else {
@ -134,9 +210,13 @@ function syncRecordings(recordings) {
r.sizeInByte = recording.sizeInByte;
r.status = recording.status;
r.startDate = recording.startDate;
r.ko_progressString(recording.progress === -1 ? '' : (recording.progress + '%'));
r.ko_size(calculateSize(recording.sizeInByte));
r.ko_status(recording.status);
r.ko_contactSheet(
recording.associatedFiles && recording.associatedFiles.find(file =>
file.endsWith('.jpg') || file.endsWith('.png')
) || null
);
}
}
}
@ -204,4 +284,27 @@ function updateDiskSpace() {
if (console)
console.log(textStatus, errorThrown);
});
}
}
function getImageUrl(localFilePath) {
// if (!localFilePath) return null; // Can be removed since button is disabled if no valid content
if (console) console.log("getImageUrl: " + localFilePath);
// Find recordingsDir from observableSettingsArray
let recordingsDirEntry = ko.utils.arrayFirst(observableSettingsArray(), item => item.key === 'recordingsDir');
if (!recordingsDirEntry) return null; // Can be removed since it has to exist in the config
let recordingsDir = recordingsDirEntry.ko_value();
// Normalize paths to use forward slashes
const normalizedLocalPath = localFilePath.replace(/\\/g, '/');
const normalizedRecordingsDir = recordingsDir.replace(/\\/g, '/');
const basePath = normalizedRecordingsDir.endsWith('/') ?
normalizedRecordingsDir :
normalizedRecordingsDir + '/';
if (console) console.log("normalizedLocalPath: " + normalizedLocalPath);
if (console) console.log("normalizedRecordingsDir: " + normalizedRecordingsDir);
if (console) console.log("basePath: " + basePath);
// Check if localFilePath starts with recordingsDir and replace it
if (normalizedLocalPath.startsWith(basePath)) {
return "/image/recording/" + normalizedLocalPath.substring(basePath.length);
}
return null; // Return null if the path doesn't match - in theory shouldn't happen
}