Optimize folder autocomplete with lazy loading and caching
All checks were successful
Docker Build / docker (push) Successful in 12s
All checks were successful
Docker Build / docker (push) Successful in 12s
- Lazy load folders only on input focus instead of page load - Add debouncing (150ms) to reduce API calls during typing - Implement in-memory caching for folder suggestions - Add server-side ?q= filter parameter for faster searches - Prevent concurrent duplicate folder fetch requests Improves page load time and autocomplete responsiveness.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,3 +67,4 @@ Thumbs.db
|
||||
*.tmp
|
||||
config.toml.example
|
||||
.roo/memory
|
||||
.kilocode/rules/*
|
||||
|
||||
12
README.md
12
README.md
@@ -16,7 +16,6 @@ A Python-based application for managing anime with automated monitoring, notific
|
||||
- [Features](#features)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Architecture](#architecture)
|
||||
- [Recent Updates](#recent-updates)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Installation](#installation-1)
|
||||
- [Docker Setup](#docker-setup)
|
||||
@@ -113,17 +112,6 @@ graph TD
|
||||
4. Results stored in database and notifications sent
|
||||
5. Web dashboard provides real-time monitoring and management
|
||||
|
||||
## Recent Updates
|
||||
|
||||
- **Rule Management**: Added API endpoints and UI for creating, editing, and deleting regex rules
|
||||
- **Progress Tracking**: New progress page showing in-progress file processing
|
||||
- **Flush & Reprocess**: Ability to manually reprocess files after rule changes
|
||||
- **Duplicate Episode Handling**: Improved processing logic to detect and handle duplicate episodes
|
||||
- **Title Override**: Added ability to override titles in rules
|
||||
- **Enhanced Error Logging**: Changed error logging to warnings for episode extraction and group index validation
|
||||
- **Episode Extraction Fixes**: Improved group index validation and handling in extract_episode method
|
||||
- **Command Parameter Handling**: Fixed command parameter handling for rule-specific regex index
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -6,6 +6,8 @@ import sqlite3
|
||||
import toml
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -337,9 +339,11 @@ def list_folders():
|
||||
List folders for selection when creating rules.
|
||||
Uses the configured media folder as the default root and
|
||||
optionally allows browsing within it via a ?parent= query param.
|
||||
Supports ?q= filter for searching folders by name.
|
||||
"""
|
||||
base_media = Path(config['general'].get('media_folder', '/'))
|
||||
parent_param = request.args.get('parent')
|
||||
search_query = request.args.get('q', '').lower()
|
||||
|
||||
# Determine which directory to list while ensuring we stay under media root
|
||||
current_dir = base_media
|
||||
@@ -359,6 +363,9 @@ def list_folders():
|
||||
try:
|
||||
for entry in sorted(current_dir.iterdir()):
|
||||
if entry.is_dir():
|
||||
# Apply search filter if provided
|
||||
if search_query and search_query not in entry.name.lower():
|
||||
continue
|
||||
folders.append({
|
||||
'name': entry.name,
|
||||
'path': str(entry),
|
||||
|
||||
@@ -313,8 +313,58 @@ function loadRegexHistory() {
|
||||
});
|
||||
}
|
||||
|
||||
// Populate folder suggestions for quick-add rule form
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Folder suggestions cache and debounce utility
|
||||
const folderCache = new Map();
|
||||
let folderFetchPromise = null;
|
||||
let debounceTimer = null;
|
||||
const DEBOUNCE_DELAY = 150; // ms
|
||||
|
||||
/**
|
||||
* Debounced fetch with caching for folder suggestions
|
||||
*/
|
||||
function fetchFolderSuggestions(query = '') {
|
||||
// Check cache first
|
||||
const cacheKey = query;
|
||||
if (folderCache.has(cacheKey)) {
|
||||
return Promise.resolve(folderCache.get(cacheKey));
|
||||
}
|
||||
|
||||
// Prevent duplicate concurrent requests
|
||||
if (folderFetchPromise) {
|
||||
return folderFetchPromise.then(result => {
|
||||
// Return cached or filtered result
|
||||
if (query === '') return result;
|
||||
const filtered = result.filter(f =>
|
||||
f.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
f.path.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
folderCache.set(cacheKey, filtered);
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
folderFetchPromise = fetch(`/api/folders?q=${encodeURIComponent(query)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
folderFetchPromise = null;
|
||||
if (data.error) return [];
|
||||
const folders = data.folders || [];
|
||||
folderCache.set(cacheKey, folders);
|
||||
return folders;
|
||||
})
|
||||
.catch(err => {
|
||||
folderFetchPromise = null;
|
||||
console.error('Error fetching folders:', err);
|
||||
return [];
|
||||
});
|
||||
|
||||
return folderFetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate datalist with folder options
|
||||
*/
|
||||
function populateFolderDatalist(folders) {
|
||||
const datalistId = 'animeFolderOptionsDashboard';
|
||||
let datalist = document.getElementById(datalistId);
|
||||
if (!datalist) {
|
||||
@@ -322,21 +372,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
datalist.id = datalistId;
|
||||
document.body.appendChild(datalist);
|
||||
}
|
||||
|
||||
// Clear and repopulate
|
||||
datalist.innerHTML = '';
|
||||
folders.forEach(folder => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = folder.path;
|
||||
opt.label = folder.name;
|
||||
datalist.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/api/folders')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.folders) return;
|
||||
data.folders.forEach(folder => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = folder.path;
|
||||
opt.textContent = folder.name;
|
||||
datalist.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignore errors; manual typing still works
|
||||
});
|
||||
/**
|
||||
* Initialize folder autocomplete on input focus
|
||||
*/
|
||||
function initFolderAutocomplete() {
|
||||
const input = document.querySelector('input[name="anime_folder"]');
|
||||
if (!input) return;
|
||||
|
||||
let initialized = false;
|
||||
|
||||
input.addEventListener('focus', async () => {
|
||||
if (initialized) return;
|
||||
|
||||
// Load initial suggestions on first focus
|
||||
try {
|
||||
const folders = await fetchFolderSuggestions();
|
||||
populateFolderDatalist(folders);
|
||||
initialized = true;
|
||||
} catch (err) {
|
||||
console.error('Failed to load folders:', err);
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
// Debounced search on input
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const query = input.value.trim();
|
||||
if (query.length >= 2) {
|
||||
// Search with query
|
||||
const folders = await fetchFolderSuggestions(query);
|
||||
populateFolderDatalist(folders);
|
||||
} else if (initialized) {
|
||||
// Reset to full list when query is short
|
||||
const folders = await fetchFolderSuggestions();
|
||||
populateFolderDatalist(folders);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
// Populate folder suggestions for quick-add rule form
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize folder autocomplete (lazy loaded on focus)
|
||||
initFolderAutocomplete();
|
||||
|
||||
// Initialize regex source UI
|
||||
handleRegexSourceChange();
|
||||
|
||||
Reference in New Issue
Block a user