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
All checks were successful
Docker Build / docker (push) Successful in 14s
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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'):
|
||||
|
||||
14
web/app.py
14
web/app.py
@@ -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,
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user