feat: initialise project structure with core encoding queue, database models, and web management API
Build / Build and Push (push) Successful in 50s

This commit is contained in:
2026-06-05 19:53:31 +01:00
parent dceba72891
commit 07aa8e5ffe
33 changed files with 2911 additions and 1 deletions
+152
View File
@@ -0,0 +1,152 @@
{{define "content"}}
<div style="display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap;">
<div class="card" style="flex: 2; min-width: 600px;">
<h2>Configured Watch Folders</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Path</th>
<th>Type</th>
<th>Resolution</th>
<th>Flags</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="folders-body">
<!-- Populated by JS -->
</tbody>
</table>
</div>
</div>
<div class="card" style="flex: 1; min-width: 300px;">
<h2>Add Watch Folder</h2>
<form id="add-folder-form" onsubmit="addFolder(event)">
<div class="form-group">
<label>Folder Path</label>
<input type="text" id="folder_path" class="form-control" placeholder="/media/movies" required>
</div>
<div class="form-group">
<label>Media Type</label>
<select id="media_type" class="form-control" required>
<option value="video">Video</option>
<option value="audio">Audio</option>
</select>
</div>
<div class="form-group">
<label>Target Resolution (Video only)</label>
<select id="target_resolution" class="form-control">
<option value="">Original</option>
<option value="1080p">1080p</option>
<option value="720p">720p</option>
<option value="480p">480p</option>
</select>
</div>
<div class="form-group" id="video_crf_group">
<label>CRF (Video only, 0-51)</label>
<input type="number" id="crf" class="form-control" placeholder="22" min="0" max="51">
</div>
<div class="form-group" id="video_preset_group">
<label>Preset (Video only)</label>
<select id="preset" class="form-control">
<option value="">Default (medium)</option>
<option value="ultrafast">ultrafast</option>
<option value="superfast">superfast</option>
<option value="veryfast">veryfast</option>
<option value="faster">faster</option>
<option value="fast">fast</option>
<option value="medium">medium</option>
<option value="slow">slow</option>
<option value="slower">slower</option>
<option value="veryslow">veryslow</option>
</select>
</div>
<div class="form-group">
<label>Custom FFmpeg Flags (Optional)</label>
<input type="text" id="custom_ffmpeg_flags" class="form-control" placeholder="-vcodec copy">
</div>
<button type="submit" class="btn" style="width: 100%;">Add Folder</button>
</form>
</div>
</div>
<script>
const foldersBody = document.getElementById('folders-body');
async function loadFolders() {
const res = await fetch('/api/folders');
const folders = await res.json();
renderFolders(folders || []);
}
function renderFolders(folders) {
foldersBody.innerHTML = folders.map(f => `
<tr>
<td style="word-break: break-all;">${f.folder_path}</td>
<td><span class="badge" style="background: var(--surface-hover); color: var(--text-primary); border-color: var(--border);">${f.media_type}</span></td>
<td>${f.target_resolution || '-'}</td>
<td><code style="background: var(--bg); padding: 0.2rem; border-radius: 4px;">${f.custom_ffmpeg_flags || '-'}</code></td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteFolder(${f.id})">Delete</button>
</td>
</tr>
`).join('');
}
async function addFolder(e) {
e.preventDefault();
let flags = document.getElementById('custom_ffmpeg_flags').value;
const crf = document.getElementById('crf').value;
const preset = document.getElementById('preset').value;
const mediaType = document.getElementById('media_type').value;
if (mediaType === 'video') {
if (crf) {
flags += (flags ? ' ' : '') + '-crf ' + crf;
}
if (preset) {
flags += (flags ? ' ' : '') + '-preset ' + preset;
}
}
const payload = {
folder_path: document.getElementById('folder_path').value,
media_type: mediaType,
target_resolution: document.getElementById('target_resolution').value,
custom_ffmpeg_flags: flags,
};
const res = await fetch('/api/folders/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
document.getElementById('add-folder-form').reset();
loadFolders();
} else {
alert('Failed to add folder');
}
}
async function deleteFolder(id) {
if (!confirm('Delete watch folder?')) return;
const res = await fetch('/api/folders/delete/' + id, { method: 'DELETE' });
if (res.ok) {
loadFolders();
} else {
alert('Failed to delete folder');
}
}
loadFolders();
document.getElementById('media_type').addEventListener('change', function (e) {
const isVideo = e.target.value === 'video';
document.getElementById('target_resolution').parentElement.style.display = isVideo ? 'block' : 'none';
document.getElementById('video_crf_group').style.display = isVideo ? 'block' : 'none';
document.getElementById('video_preset_group').style.display = isVideo ? 'block' : 'none';
});
</script>
{{end}}