Refactor rule handling: rename regex_pattern to match_regex and add rename_regex; update database schema and UI accordingly
All checks were successful
Docker Build / docker (push) Successful in 14s

This commit is contained in:
2026-02-02 23:46:29 +07:00
parent 0d85012dbc
commit 1ca6f94ee6
6 changed files with 104 additions and 37 deletions

View File

@@ -19,7 +19,7 @@ Anime Manager is a Python file processor with web dashboard that monitors a drop
### 2. Rules & Database (core/database.py)
- **Schema:** `rules` table stores regex patterns per anime folder; `processed_files` tracks hash/status; `activity_log` for audit trail
- **Rules have:** regex_pattern, anime_folder destination, optional override_title, episode_group number, enabled flag
- **Rules have:** match_regex (drop folder matching), rename_regex (rename.py output), anime_folder destination, optional override_title, episode_group number, enabled flag
- **File states:** pending → processing → {copied, renamed, no_match, copy_error, etc.}
- **Query pattern:** Always use `enabled_only=True` when fetching active rules for processing

View File

@@ -15,7 +15,8 @@ class Database:
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anime_folder TEXT NOT NULL,
regex_pattern TEXT NOT NULL,
match_regex TEXT NOT NULL,
rename_regex TEXT NOT NULL,
regex_index INTEGER,
offset INTEGER DEFAULT 0,
override_title TEXT,
@@ -25,6 +26,35 @@ class Database:
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Migration: upgrade old schema (regex_pattern) to new (match_regex, rename_regex)
try:
conn.execute("SELECT match_regex FROM rules LIMIT 1")
except sqlite3.OperationalError:
try:
conn.execute("SELECT regex_pattern FROM rules LIMIT 1")
conn.execute('''
CREATE TABLE rules_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
anime_folder TEXT NOT NULL,
match_regex TEXT NOT NULL,
rename_regex TEXT NOT NULL,
regex_index INTEGER,
offset INTEGER DEFAULT 0,
override_title TEXT,
episode_group INTEGER DEFAULT 2,
enabled BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.execute('''
INSERT INTO rules_new (id, anime_folder, match_regex, rename_regex, regex_index, offset, override_title, episode_group, enabled, created_at, updated_at)
SELECT id, anime_folder, regex_pattern, regex_pattern, regex_index, offset, override_title, episode_group, enabled, created_at, updated_at FROM rules
''')
conn.execute("DROP TABLE rules")
conn.execute("ALTER TABLE rules_new RENAME TO rules")
except sqlite3.OperationalError:
pass
# Processed files tracking
conn.execute('''
@@ -55,12 +85,12 @@ class Database:
conn.commit()
def add_rule(self, anime_folder, regex_pattern, **kwargs):
def add_rule(self, anime_folder, match_regex, rename_regex, **kwargs):
with sqlite3.connect(self.db_path) as conn:
cursor = conn.execute('''
INSERT INTO rules (anime_folder, regex_pattern, regex_index, offset, override_title, episode_group, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (anime_folder, regex_pattern,
INSERT INTO rules (anime_folder, match_regex, rename_regex, regex_index, offset, override_title, episode_group, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (anime_folder, match_regex, rename_regex,
kwargs.get('regex_index'), kwargs.get('offset', 0),
kwargs.get('override_title'), kwargs.get('episode_group', 2),
kwargs.get('enabled', True)))

View File

@@ -44,11 +44,11 @@ class FileProcessor:
return videos
def match_rule(self, filename: str) -> Optional[Dict]:
"""Match filename against rules. Returns rule dict or None"""
"""Match filename against rules (using match_regex). Returns rule dict or None"""
rules = self.db.get_rules(enabled_only=True)
for rule in rules:
if re.search(rule['regex_pattern'], filename, re.IGNORECASE):
if re.search(rule['match_regex'], filename, re.IGNORECASE):
return rule
return None
@@ -131,9 +131,9 @@ class FileProcessor:
'--execute'
]
# Add rule-specific parameters
if rule.get('regex_pattern'):
cmd.extend(['-r', rule['regex_pattern']])
# Add rule-specific parameters (rename_regex for rename.py output format)
if rule.get('rename_regex'):
cmd.extend(['-r', rule['rename_regex']])
if rule.get('regex_index') is not None:
cmd.extend(['-x', str(rule['regex_index'])])
if rule.get('offset'):

View File

@@ -43,9 +43,19 @@ def add_rule():
regex_index_val = data.get('regex_index')
regex_index = int(regex_index_val) if regex_index_val and regex_index_val.strip() else None
match_regex = data.get('match_regex', '').strip() or data.get('regex_pattern', '').strip()
rename_regex = data.get('rename_regex', '').strip() or data.get('regex_pattern', '').strip()
if not match_regex:
match_regex = rename_regex
if not rename_regex:
rename_regex = match_regex
if not match_regex:
return "Match regex and rename regex are required", 400
rule_id = db.add_rule(
anime_folder=data['anime_folder'],
regex_pattern=data['regex_pattern'],
match_regex=match_regex,
rename_regex=rename_regex,
regex_index=regex_index,
offset=int(data.get('offset', 0)),
override_title=data.get('override_title') or None,
@@ -86,7 +96,7 @@ def test_regex():
rules = db.get_rules(enabled_only=True)
for rule in rules:
try:
match = re.search(rule['regex_pattern'], filename, re.IGNORECASE)
match = re.search(rule['match_regex'], filename, re.IGNORECASE)
if match:
return jsonify({
'match': True,

View File

@@ -68,7 +68,7 @@
<div class="card">
<div class="card-header">Quick Add Rule</div>
<div class="card-body">
<form action="/rules/add" method="post">
<form action="/rules/add" method="post" id="quickAddRuleForm" onsubmit="return syncRenameRegex()">
<div class="mb-3">
<label>Anime Folder Path</label>
<input type="text"
@@ -79,13 +79,19 @@
placeholder="/media/Anime/[anidbN-1234] Title">
</div>
<div class="mb-3">
<label>Regex Pattern</label>
<label>Match Regex <span class="text-danger">*</span></label>
<input type="text" name="match_regex" class="form-control" required
placeholder=".*SubsPlease.*1080p.*">
<div class="form-text">Matches incoming files in drop folder.</div>
</div>
<div class="mb-3">
<label>Rename Regex <span class="text-danger">*</span></label>
<select id="regex_source" class="form-select mb-2" onchange="handleRegexSourceChange()">
<option value="custom">Custom (write your own)</option>
<option value="none">None (no regex)</option>
<option value="same">Same as Match Regex</option>
<option value="history" disabled>Saved regex (loading...)</option>
</select>
<input type="text" id="regex_custom" name="regex_pattern" class="form-control" placeholder=".*SubsPlease.*1080p.*">
<input type="text" id="regex_custom" name="rename_regex" class="form-control" placeholder=".*SubsPlease.*- (\d+)\.mkv">
<input type="hidden" id="regex_index" name="regex_index" value="">
<div id="regex_history_list" class="mt-2" style="display:none;">
<select id="regex_history_select" class="form-select" onchange="handleHistorySelect()">
@@ -94,7 +100,7 @@
<div class="form-text" id="regex_preview"></div>
</div>
<div class="form-text">
Choose a saved regex from rename.py history, write your own, or leave empty.
For rename.py output. From history, custom, or same as match.
</div>
</div>
<button type="submit" class="btn btn-success w-100">Add Rule</button>
@@ -111,22 +117,33 @@ function processNow() {
.then(d => alert(`Processed ${d.processed} files`));
}
function syncRenameRegex() {
const source = document.getElementById('regex_source').value;
const renameInput = document.getElementById('regex_custom');
const matchInput = document.querySelector('input[name="match_regex"]');
if (source === 'same' && matchInput) {
renameInput.value = matchInput.value;
renameInput.removeAttribute('required');
}
return true;
}
function handleRegexSourceChange() {
const source = document.getElementById('regex_source').value;
const customInput = document.getElementById('regex_custom');
const historyDiv = document.getElementById('regex_history_list');
const historySelect = document.getElementById('regex_history_select');
const regexIndexInput = document.getElementById('regex_index');
const regexPatternInput = customInput; // same as name="regex_pattern"
const renamePatternInput = customInput; // same as name="rename_regex"
// Hide all
customInput.style.display = 'none';
historyDiv.style.display = 'none';
regexPatternInput.required = false;
renamePatternInput.required = false;
regexIndexInput.value = '';
// Clear custom input when not in custom mode
if (source !== 'custom') {
regexPatternInput.value = '';
renamePatternInput.value = '';
}
// Clear history selection when not in history mode
if (source !== 'history') {
@@ -136,32 +153,30 @@ function handleRegexSourceChange() {
if (source === 'custom') {
customInput.style.display = 'block';
regexPatternInput.required = true;
renamePatternInput.required = true;
} else if (source === 'history') {
historyDiv.style.display = 'block';
// Ensure history is loaded
loadRegexHistory();
} else if (source === 'none') {
// No regex, leave both empty
}
// "same" - no input needed, will copy from match on submit
}
function handleHistorySelect() {
const select = document.getElementById('regex_history_select');
const preview = document.getElementById('regex_preview');
const regexIndexInput = document.getElementById('regex_index');
const regexPatternInput = document.getElementById('regex_custom');
const renamePatternInput = document.getElementById('regex_custom');
const selectedOption = select.options[select.selectedIndex];
if (selectedOption.value === '') {
regexIndexInput.value = '';
regexPatternInput.value = '';
renamePatternInput.value = '';
preview.textContent = '';
return;
}
const index = selectedOption.value;
const pattern = selectedOption.text;
regexIndexInput.value = index;
regexPatternInput.value = pattern; // set pattern for backend
renamePatternInput.value = pattern; // set pattern for backend
preview.textContent = `Selected regex: ${pattern}`;
}

View File

@@ -42,7 +42,8 @@
<tr>
<th>ID</th>
<th>Anime Folder</th>
<th>Regex Pattern</th>
<th>Match Regex</th>
<th>Rename Regex</th>
<th>Settings</th>
<th>Status</th>
<th>Actions</th>
@@ -59,8 +60,13 @@
{% endif %}
</td>
<td>
<code class="text-primary">{{ rule.regex_pattern }}</code>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyRegex({{ rule.regex_pattern | tojson }})">Copy</button> </td>
<code class="text-primary" title="Matches drop folder files">{{ rule.match_regex }}</code>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyRegex({{ rule.match_regex | tojson }})">Copy</button>
</td>
<td>
<code class="text-info" title="For rename.py output">{{ rule.rename_regex }}</code>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyRegex({{ rule.rename_regex | tojson }})">Copy</button>
</td>
<td>
{% if rule.offset != 0 %}<span class="badge bg-secondary">Offset: {{ rule.offset }}</span>{% endif %}
{% if rule.episode_group != 2 %}<span class="badge bg-secondary">Group: {{ rule.episode_group }}</span>{% endif %}
@@ -81,7 +87,7 @@
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center text-muted py-4">
<td colspan="7" class="text-center text-muted py-4">
No rules configured yet. Add your first rule to get started.
</td>
</tr>
@@ -116,11 +122,17 @@
</div>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Regex Pattern <span class="text-danger">*</span></label>
<input type="text" name="regex_pattern" class="form-control font-monospace" required
placeholder=".*SubsPlease.*1080p.*- (\d+)\.mkv">
<div class="form-text">Python regex pattern to match filenames. Use capture groups for episode numbers.</div>
<div class="col-md-6 mb-3">
<label class="form-label">Match Regex <span class="text-danger">*</span></label>
<input type="text" name="match_regex" class="form-control font-monospace" required
placeholder=".*SubsPlease.*1080p.*">
<div class="form-text">Matches incoming files in drop folder. Route to this rule when filename matches.</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Rename Regex <span class="text-danger">*</span></label>
<input type="text" name="rename_regex" class="form-control font-monospace" required
placeholder=".*SubsPlease.*- (\d+)\.mkv">
<div class="form-text">For rename.py: extract episode numbers. Use capture groups.</div>
</div>
<div class="col-md-4 mb-3">