Add configurable filename import filters
CI / verify (push) Failing after 14m47s

Add include and exclude filename conditions across every new import path, show accepted and excluded counts before destination selection, and keep restored queues unchanged.

Refine the settings layout and reorder upload telemetry for faster status reading.
This commit is contained in:
Sucukdeluxe
2026-08-15 05:13:14 +02:00
parent b286574902
commit f1774b50f8
14 changed files with 514 additions and 42 deletions
+177 -33
View File
@@ -651,12 +651,14 @@ async function init() {
const name = p.split('\\').pop().split('/').pop();
newFiles.push({ path: p, name, size: null });
}
if (newFiles.length > 0) {
const newPaths = new Set(newFiles.map(f => f.path));
const admitted = admitFilenameFilter(newFiles);
if (admitted.accepted.length > 0) {
const newPaths = new Set(admitted.accepted.map(f => f.path));
clearDedupKeysForPaths(newPaths);
selectedFiles.push(...newFiles);
selectedFiles.push(...admitted.accepted);
buildQueuePreview();
updateUploadView();
if (admitted.active && admitted.excluded.length > 0) showFilenameFilterResult(admitted);
if (fm.autoStart && !uploading && !healthCheckRunning) {
startUpload();
} else if (uploading) {
@@ -666,6 +668,8 @@ async function init() {
await startSelectedUpload(newJobs);
}
}
} else if (admitted.active && admitted.total > 0) {
showFilenameFilterResult(admitted);
}
} else {
// No pre-selected hosters: open modal
@@ -1260,6 +1264,12 @@ function renderHosterModal() {
function openHosterModal() {
syncSelectedUploadHosters();
renderHosterModal();
const description = document.getElementById('hosterModalDescription');
if (description) {
description.textContent = _pendingImportSummary
? formatFilenameFilterResult(_pendingImportSummary)
: localizeUiText('Dateien wurden hinzugefügt. Wähle jetzt die Hoster für den Upload.');
}
modalController.open('hosterModal', {
initialFocus: '#cancelHosterModalBtn',
fallbackFocus: '#addFilesBtn',
@@ -1298,10 +1308,12 @@ async function applyHosterSelection() {
updateUploadView();
persistQueueStateSoon(true); // immediate persist after adding files
closeHosterModal();
_pendingImportSummary = null;
}
function cancelHosterModal() {
_pendingFiles = [];
_pendingImportSummary = null;
closeHosterModal();
}
@@ -1538,9 +1550,37 @@ function setupDragDrop() {
}
let _pendingFiles = []; // Files waiting for hoster modal confirmation
let _pendingImportSummary = null;
let _addingDropped = false;
function admitFilenameFilter(files) {
return window.FilenameFilter.applyFilenameFilter(files, config?.globalSettings?.filenameFilter);
}
function mergePendingImportSummary(result) {
if (!result.active) {
if (!_pendingImportSummary) _pendingImportSummary = null;
return;
}
const current = _pendingImportSummary || { total: 0, accepted: 0, excluded: 0 };
_pendingImportSummary = {
total: current.total + result.total,
accepted: current.accepted + result.accepted.length,
excluded: current.excluded + result.excluded.length
};
}
function formatFilenameFilterResult(result) {
const accepted = Array.isArray(result.accepted) ? result.accepted.length : Number(result.accepted) || 0;
const excluded = Array.isArray(result.excluded) ? result.excluded.length : Number(result.excluded) || 0;
return localizeUiText(`${accepted} von ${result.total} Dateien werden hinzugefügt. ${excluded} durch den Dateinamenfilter ausgeschlossen.`);
}
function showFilenameFilterResult(result) {
showCopyToast(formatFilenameFilterResult(result), 6500);
}
async function addDropTargetEntries(entries) {
const files = [];
for (const entry of Array.isArray(entries) ? entries : []) {
@@ -1563,12 +1603,7 @@ async function addDroppedFiles(fileList) {
_addingDropped = true;
try {
const files = Array.from(fileList);
const existingPaths = new Set([
...selectedFiles.map(f => f.path),
..._pendingFiles.map(f => f.path)
]);
const newFiles = [];
let duplicateCount = 0;
const entries = [];
for (const file of files) {
let filePath = '';
@@ -1583,14 +1618,9 @@ async function addDroppedFiles(fileList) {
for (const fp of folderFiles) {
const p = typeof fp === 'string' ? fp : (fp && fp.path);
if (!p) continue;
if (existingPaths.has(p)) {
duplicateCount++;
continue;
}
const name = typeof fp === 'string' ? p.split('\\').pop().split('/').pop() : (fp.name || p.split('\\').pop().split('/').pop());
const size = typeof fp === 'string' ? null : (fp.size || 0);
newFiles.push({ path: p, name, size });
existingPaths.add(p);
entries.push({ path: p, name, size });
}
continue;
}
@@ -1599,20 +1629,9 @@ async function addDroppedFiles(fileList) {
// Regular file
const fileName = file.name || '';
if (!existingPaths.has(filePath)) {
newFiles.push({ path: filePath, name: fileName, size: file.size });
existingPaths.add(filePath);
} else {
duplicateCount++;
}
}
if (newFiles.length > 0) {
_pendingFiles.push(...newFiles);
openHosterModal();
} else if (duplicateCount > 0) {
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
entries.push({ path: filePath, name: fileName, size: file.size });
}
addPathsToQueue(entries);
} finally {
_addingDropped = false;
}
@@ -1648,11 +1667,15 @@ function addPathsToQueue(paths) {
newFiles.push({ path: p, name, size });
if (size === null || size === undefined || size === 0) pendingSizeFetch.push(p);
}
if (newFiles.length > 0) {
_pendingFiles.push(...newFiles);
const admitted = admitFilenameFilter(newFiles);
if (admitted.accepted.length > 0) {
const acceptedPaths = new Set(admitted.accepted.map(file => file.path));
const acceptedSizeFetch = pendingSizeFetch.filter(filePath => acceptedPaths.has(filePath));
_pendingFiles.push(...admitted.accepted);
mergePendingImportSummary(admitted);
openHosterModal();
if (pendingSizeFetch.length > 0 && window.api.getFileSizes) {
window.api.getFileSizes(pendingSizeFetch).then((sizeMap) => {
if (acceptedSizeFetch.length > 0 && window.api.getFileSizes) {
window.api.getFileSizes(acceptedSizeFetch).then((sizeMap) => {
if (!sizeMap || typeof sizeMap !== 'object') return;
let changed = false;
for (const f of _pendingFiles) {
@@ -1671,6 +1694,10 @@ function addPathsToQueue(paths) {
}
}).catch(() => {});
}
} else if (admitted.active && admitted.total > 0) {
showFilenameFilterResult(admitted);
} else if (Array.isArray(paths) && paths.length > 0) {
showCopyToast('Auswahl ist bereits in den Upload-Aufträgen.');
}
}
@@ -4605,6 +4632,79 @@ async function _renderLogPathsList(el) {
}
}
function filenameFilterConditionRowHtml(condition = {}) {
const operator = condition.operator === 'notContains' ? 'notContains' : 'contains';
return `
<div class="filename-filter-condition" data-filename-filter-condition>
<div class="filename-filter-rule-field">
<span class="filename-filter-rule-label" data-filename-filter-value-label>Dateiname filtern</span>
<input type="text" class="key-input settings-autosave" data-filename-filter-value value="${escapeAttr(condition.value || '')}" placeholder="z. B. 720p" aria-label="Dateiname filtern">
</div>
<div class="filename-filter-rule-field">
<span class="filename-filter-rule-label" data-filename-filter-operator-label>Vergleich</span>
<select class="hs-input settings-autosave" data-filename-filter-operator aria-label="Dateinamen-Bedingung">
<option value="contains" ${operator === 'contains' ? 'selected' : ''}>enthält</option>
<option value="notContains" ${operator === 'notContains' ? 'selected' : ''}>enthält nicht</option>
</select>
</div>
<button type="button" class="btn btn-xs btn-danger" data-filename-filter-remove aria-label="Bedingung entfernen">Entfernen</button>
</div>`;
}
function readFilenameFilterSettings() {
const conditions = Array.from(document.querySelectorAll('[data-filename-filter-condition]')).map(row => ({
operator: row.querySelector('[data-filename-filter-operator]')?.value || 'contains',
value: row.querySelector('[data-filename-filter-value]')?.value || ''
}));
return window.FilenameFilter.normalizeFilenameFilter({
enabled: document.getElementById('filenameFilterEnabledInput')?.checked === true,
action: document.getElementById('filenameFilterActionInput')?.value,
matchMode: document.getElementById('filenameFilterMatchModeInput')?.value,
conditions
});
}
function syncFilenameFilterControls() {
const enabled = document.getElementById('filenameFilterEnabledInput')?.checked === true;
const panel = document.getElementById('filenameFilterBuilder');
if (panel) panel.classList.toggle('disabled', !enabled);
const rows = Array.from(document.querySelectorAll('[data-filename-filter-condition]'));
document.querySelectorAll('#filenameFilterBuilder select, #filenameFilterBuilder input, #addFilenameFilterConditionBtn').forEach(control => {
control.disabled = !enabled;
});
rows.forEach(row => {
const remove = row.querySelector('[data-filename-filter-remove]');
if (remove) remove.disabled = !enabled || rows.length === 1;
});
}
function wireFilenameFilterConditionRow(row) {
if (!row || row.dataset.wired === 'true') return;
row.dataset.wired = 'true';
row.querySelectorAll('.settings-autosave').forEach(control => {
const eventName = control.tagName === 'SELECT' ? 'change' : 'input';
control.addEventListener(eventName, markSettingsDirty);
});
row.querySelector('[data-filename-filter-remove]')?.addEventListener('click', () => {
row.remove();
syncFilenameFilterControls();
markSettingsDirty();
});
}
function appendFilenameFilterCondition(condition = {}) {
const list = document.getElementById('filenameFilterConditions');
if (!list) return;
const template = document.createElement('template');
template.innerHTML = filenameFilterConditionRowHtml(condition).trim();
const row = template.content.firstElementChild;
list.appendChild(row);
wireFilenameFilterConditionRow(row);
syncFilenameFilterControls();
row.querySelector('[data-filename-filter-value]')?.focus();
markSettingsDirty();
}
function renderSettings() {
const container = document.getElementById('settingsHosters');
container.innerHTML = '';
@@ -4613,10 +4713,14 @@ function renderSettings() {
const configuredAccounts = getAvailableHosters();
const fm = globalSettings.folderMonitor || {};
const remoteSettings = globalSettings.remote || {};
const filenameFilter = window.FilenameFilter.normalizeFilenameFilter(globalSettings.filenameFilter);
const filenameFilterConditions = filenameFilter.conditions.length > 0
? filenameFilter.conditions
: [{ operator: 'contains', value: '' }];
const pageDefinitions = [
{ id: 'allgemein', label: 'Allgemein', search: 'fenster window vordergrund foreground always on top drop target oberfläche interface updates update aktualisierung version language sprache' },
{ id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange waiting fertig completed completion abschluss entfernen remove parallel geschwindigkeit speed limit fortsetzen resume wiederherstellen restore hoster' },
{ id: 'uploads', label: 'Uploads', search: 'upload queue warteschlange waiting fertig completed completion abschluss entfernen remove parallel geschwindigkeit speed limit fortsetzen resume wiederherstellen restore hoster dateiname filename filter enthält contains ausschließen exclude' },
{ id: 'automatik', label: 'Automatik', search: 'automatisch automation automatic retry wiederholen ordner folder monitor überwachen watch dateierweiterungen extensions unterordner subfolders duplikate duplicates' },
{ id: 'benachrichtigungen', label: 'Benachrichtigungen', search: 'benachrichtigungen notifications webhook discord meldung message ping erwähnung mention batch fertig completed' },
{ id: 'logs', label: 'Logs & Support', search: 'log logs protokoll logging debug verbose diagnose diagnostics support paket package datei file ordner folder' },
@@ -4748,6 +4852,41 @@ function renderSettings() {
</div>
<input type="checkbox" class="settings-autosave" id="autoStartRestoredQueueInput" ${globalSettings.autoStartRestoredQueue ? 'checked' : ''}>
</div>
<div class="settings-section-label">Dateinamenfilter</div>
<div class="filename-filter-panel">
<div class="settings-option filename-filter-toggle">
<div class="settings-option-copy">
<label for="filenameFilterEnabledInput">Dateinamen beim Hinzufügen filtern</label>
<span class="settings-option-description">Prüft neue Dateien aus Auswahl, Ordnern, Drag-and-drop und Ordnerüberwachung, bevor sie in die Upload-Liste gelangen.</span>
</div>
<input type="checkbox" class="settings-autosave" id="filenameFilterEnabledInput" ${filenameFilter.enabled ? 'checked' : ''}>
</div>
<div class="filename-filter-builder" id="filenameFilterBuilder">
<div class="filename-filter-conditions" id="filenameFilterConditions">
${filenameFilterConditions.map(filenameFilterConditionRowHtml).join('')}
</div>
<div class="filename-filter-footer">
<button type="button" class="btn btn-xs btn-secondary" id="addFilenameFilterConditionBtn">+ Bedingung</button>
<span class="hint">Groß- und Kleinschreibung werden ignoriert. Leere Bedingungen werden nicht gespeichert.</span>
</div>
<div class="filename-filter-policy">
<div class="filename-filter-field">
<label for="filenameFilterMatchModeInput">Bedingungen</label>
<select class="hs-input settings-autosave" id="filenameFilterMatchModeInput">
<option value="all" ${filenameFilter.matchMode === 'all' ? 'selected' : ''}>alle müssen passen</option>
<option value="any" ${filenameFilter.matchMode === 'any' ? 'selected' : ''}>mindestens eine muss passen</option>
</select>
</div>
<div class="filename-filter-field">
<label for="filenameFilterActionInput">Wenn passend</label>
<select class="hs-input settings-autosave" id="filenameFilterActionInput">
<option value="include" ${filenameFilter.action === 'include' ? 'selected' : ''}>Dateien hinzufügen</option>
<option value="exclude" ${filenameFilter.action === 'exclude' ? 'selected' : ''}>Dateien nicht hinzufügen</option>
</select>
</div>
</div>
</div>
</div>
<div class="settings-section-label">Quelldateien</div>
<div class="settings-option source-delete-option">
<div class="settings-option-copy">
@@ -5247,6 +5386,10 @@ function renderSettings() {
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', requestUpdateCheck);
container.querySelectorAll('[data-filename-filter-condition]').forEach(wireFilenameFilterConditionRow);
document.getElementById('addFilenameFilterConditionBtn')?.addEventListener('click', () => appendFilenameFilterCondition());
document.getElementById('filenameFilterEnabledInput')?.addEventListener('change', syncFilenameFilterControls);
syncFilenameFilterControls();
_syncHeaderUpdateState();
container.querySelectorAll('.settings-autosave').forEach((input) => {
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
@@ -5393,6 +5536,7 @@ async function performSaveSettings(options = {}) {
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
deleteSourceAfterSuccessfulUpload: elChk('deleteSourceAfterSuccessfulUploadInput', !!cur.deleteSourceAfterSuccessfulUpload),
filenameFilter: readFilenameFilterSettings(),
showDropTarget: elChk('showDropTargetInput', !!cur.showDropTarget),
globalMaxSpeedKbs: (() => {
const el = document.getElementById('globalMaxSpeedMbsInput');
+23
View File
@@ -37,6 +37,27 @@
['Fensterverhalten, Drop-Target und Programmupdates.', 'Window behavior, drop target, and application updates.'],
['Gesamt:', 'Total:'],
['Globales Speed-Limit', 'Global speed limit'],
['Dateinamenfilter', 'Filename filter'],
['Dateinamen beim Hinzufügen filtern', 'Filter filenames when adding files'],
['Prüft neue Dateien aus Auswahl, Ordnern, Drag-and-drop und Ordnerüberwachung, bevor sie in die Upload-Liste gelangen.', 'Checks new files from selections, folders, drag and drop, and folder monitoring before they enter the upload list.'],
['Treffer', 'Matches'],
['Wenn passend', 'When matched'],
['Bedingungen', 'Conditions'],
['nur hinzufügen', 'add only'],
['nicht hinzufügen', 'do not add'],
['Dateien hinzufügen', 'Add files'],
['Dateien nicht hinzufügen', 'Do not add files'],
['alle müssen passen', 'all must match'],
['mindestens eine muss passen', 'at least one must match'],
['Dateinamen-Bedingung', 'Filename condition'],
['Vergleich', 'Comparison'],
['enthält', 'contains'],
['enthält nicht', 'does not contain'],
['Dateiname filtern', 'Filter file name'],
['z. B. 720p', 'e.g. 720p'],
['Bedingung entfernen', 'Remove condition'],
['+ Bedingung', '+ Condition'],
['Groß- und Kleinschreibung werden ignoriert. Leere Bedingungen werden nicht gespeichert.', 'Matching ignores letter case. Empty conditions are not saved.'],
['Hauptbereiche', 'Main sections'],
['Hinweis', 'Notice'],
['Hoster-Limits automatisch hochskalieren', 'Automatically scale host limits'],
@@ -663,6 +684,7 @@
[/^(\d+) hinzugefügt$/, '$1 added'],
[/^(\d+) bereits im Batch$/, '$1 already in the batch'],
[/^(\d+) ohne gültigen Account$/, '$1 without a valid account'],
[/^(\d+) von (\d+) Dateien werden hinzugefügt\. (\d+) durch den Dateinamenfilter ausgeschlossen\.$/, '$1 of $2 files will be added. $3 excluded by the filename filter.'],
[/^Sitzungsbericht mit 1 Upload exportiert$/, 'Session report with 1 upload exported'],
[/^Sitzungsbericht mit (\d+) Uploads exportiert$/, 'Session report with $1 uploads exported'],
[/^Login ok, Upload-Form bereit \(Dateifeld: (.+)\)$/, 'Login successful, upload form ready (file field: $1)'],
@@ -782,6 +804,7 @@
[/^(\d+) added$/, '$1 hinzugefügt'],
[/^(\d+) already in the batch$/, '$1 bereits im Batch'],
[/^(\d+) without a valid account$/, '$1 ohne gültigen Account'],
[/^(\d+) of (\d+) files will be added\. (\d+) excluded by the filename filter\.$/, '$1 von $2 Dateien werden hinzugefügt. $3 durch den Dateinamenfilter ausgeschlossen.'],
[/^Session report with 1 upload exported$/, 'Sitzungsbericht mit 1 Upload exportiert'],
[/^Session report with (\d+) uploads exported$/, 'Sitzungsbericht mit $1 Uploads exportiert'],
[/^Sleep in (\d+)s\.\.\.$/, 'Ruhezustand in $1s...'],
+3 -2
View File
@@ -209,10 +209,10 @@
<div class="view-sidebar-summary view-sidebar-summary-block hoster-summary" id="hosterSummary">Keine Upload-Ziele ausgewählt</div>
</div>
<div class="upload-telemetry" id="uploadTelemetry" aria-label="Upload-Statistik">
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Gesamt</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryTotal" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbindungen</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryConnections" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbleibend</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRemaining" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Gesamt</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryTotal" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Läuft</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryRunning" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Verbindungen</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryConnections" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fertig</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryCompleted" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Fehler</span><strong class="upload-telemetry-value upload-rolling-value" id="uploadTelemetryFailed" data-numeric-value="0" aria-label="0"><span>0</span></strong></div>
<div class="upload-telemetry-row"><span class="upload-telemetry-label">Geschwindigkeit</span><strong class="upload-telemetry-value" id="uploadTelemetrySpeed" aria-label="0 B/s">0 B/s</strong></div>
@@ -683,6 +683,7 @@
<script src="../lib/serialized-runner.js"></script>
<script src="../lib/speed-history.js"></script>
<script src="../lib/upload-recovery.js"></script>
<script src="../lib/filename-filter.js"></script>
<script src="account-submit.js"></script>
<script src="account-status.js"></script>
<script src="history-status.js"></script>
+97
View File
@@ -3303,6 +3303,103 @@ input[type="checkbox"] {
border-color: var(--danger);
}
.filename-filter-panel {
display: grid;
gap: 10px;
}
.filename-filter-builder {
display: grid;
gap: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-card);
transition: opacity 160ms ease, border-color 160ms ease;
}
.filename-filter-builder.disabled {
opacity: 0.55;
}
.filename-filter-policy {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.filename-filter-field {
display: grid;
gap: 6px;
}
.filename-filter-field label {
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
}
.filename-filter-policy .hs-input,
.filename-filter-condition .hs-input,
.filename-filter-condition .key-input {
min-width: 0;
max-width: none;
width: 100%;
}
.filename-filter-conditions {
display: grid;
gap: 8px;
}
.filename-filter-condition {
display: grid;
grid-template-columns: minmax(240px, 1.35fr) minmax(150px, 0.65fr) auto;
gap: 8px;
align-items: end;
}
.filename-filter-rule-field {
display: grid;
gap: 6px;
min-width: 0;
}
.filename-filter-rule-label {
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
}
.filename-filter-footer {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
}
.filename-filter-footer .hint {
margin-left: auto;
text-align: right;
}
@media (max-width: 820px) {
.filename-filter-policy,
.filename-filter-condition {
grid-template-columns: 1fr;
}
.filename-filter-footer {
align-items: stretch;
flex-direction: column;
}
.filename-filter-footer .hint {
margin-left: 0;
text-align: left;
}
}
.settings-option-description,
.hint {
color: var(--text-dim);