Skip to content

Commit 8638224

Browse files
feat(fts): Add comprehensive Full Text Search management to CWA Settings
Implemented advanced FTS control interface in CWA Settings page with real-time status monitoring, replacing basic checkbox in Basic Configuration with full management capabilities including progress tracking and resource information. Backend Changes (cps/cwa_functions.py): - Added /fts-status GET route: Returns JSON with enabled/indexed/total/percentage/ available/active status, polling-friendly - Added /fts-action POST route: Handles enable/disable/reindex actions with proper subprocess confirmation handling - Added _get_fts_status_from_calibredb(): Parses calibredb output, handles both exit code 0 (enabled) and 2 (disabled) as valid responses - Added _initialize_fts_index(): Enable/disable FTS with validation, checks actual status before skipping enable when database exists - Fixed duplicate_auto_resolve_cooldown_minutes handling: Added validation (0-1440), default value (0), and fallback logic in integer settings processing Frontend Changes (cps/templates/cwa_settings.html): - New FTS Management section with live status display showing current state (Active/Not Available/Disabled/Error) with emoji indicators - Progress tracking: "X of Y books indexed (Z%)" with 90% threshold for availability - Action buttons: Enable/Disable/Reindex/Refresh with proper state management - JavaScript polling: 10-second intervals with visibility detection, stops during actions - Network share warning: Static info box about slower indexing on NFS/SMB - Disable behavior warning: Prominent alert that disabling requires complete re-indexing (Calibre design limitation) - Collapsible resource info: Details on CPU/disk/memory usage, performance characteristics, large library considerations Configuration Page Updates (cps/templates/config_edit.html): - Added tooltip to FTS checkbox: "Manage FTS indexing in CWA Settings for advanced options" - Added link to CWA Settings page for full management interface Bug Fixes: - Fixed Jinja2 translation errors: Removed colons from {{_()}} calls, escaped %% symbols - Added @csrf.exempt to /fts-action route for AJAX POST requests - Fixed calibredb subprocess confirmations: * disable command requires input='disable\n' * reindex command requires input='reindex\n' - Fixed status parsing to handle "FTS Indexing is disabled" output (no book counts) - Fixed duplicate_auto_resolve_cooldown_minutes displaying "False" instead of number - Fixed enable command check to verify actual status when database exists but disabled Technical Details: - Exit codes: calibredb returns 0 when enabled, 2 when disabled (both valid) - Status outputs: "X of Y books files indexed" (enabled) vs "FTS Indexing is disabled" - 90% threshold: Search only activates when ≥90% of library indexed - Active detection: Compares two sequential status checks to detect ongoing indexing - Environment: All calibredb commands use LD_LIBRARY_PATH=/app/calibre/lib Known Calibre Limitations: - Disabling FTS invalidates the index, requiring full re-indexing on re-enable (this is Calibre's design for index consistency, not a CWA bug) - No pause/resume functionality available in calibredb fts_index commands
1 parent d2f0248 commit 8638224

9 files changed

Lines changed: 741 additions & 12 deletions

File tree

CONTRIBUTORS

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Copyright (C) 2018-2026 Calibre-Web contributors
1111
Copyright (C) 2024-2026 Calibre-Web Automated contributors
1212
# Upstream Contributors (janeczku/calibre-web)
1313

14-
- OzzieIsaacs (anon) (2788 commits)
15-
- Ozzie Isaacs (anon) (267 commits)
14+
- OzzieIsaacs (anon) (2794 commits)
15+
- Ozzie Isaacs (anon) (269 commits)
1616
- cbartondock (96 commits)
1717
- idalin (69 commits)
1818
- cervinko (68 commits)
@@ -238,6 +238,7 @@ Copyright (C) 2024-2026 Calibre-Web Automated contributors
238238
- kianmeng (1 commits)
239239
- knobunc (1 commits)
240240
- Kreeblah (1 commits)
241+
- Kugeleis (1 commits)
241242
- L0garithmic (1 commits)
242243
- LawssssCat (1 commits)
243244
- lb803 (1 commits)
@@ -314,7 +315,7 @@ Copyright (C) 2024-2026 Calibre-Web Automated contributors
314315
- zhiyue (1 commits)
315316
# Fork Contributors (crocodilestick/calibre-web-automated)
316317

317-
- crocodilestick (948 commits)
318+
- crocodilestick (953 commits)
318319
- jmarmstrong1207 (73 commits)
319320
- demitrix (30 commits)
320321
- sirwolfgang (29 commits)

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,9 @@ RUN \
284284
# Add unrar from unrar stage
285285
COPY --from=unrar /usr/bin/unrar-ubuntu /usr/bin/unrar
286286

287-
# Set calibre environment variable
287+
# Set calibre environment variables
288288
ENV CALIBRE_CONFIG_DIR=/config/.config/calibre
289+
ENV LD_LIBRARY_PATH=/app/calibre/lib
289290

290291
# Ports and volumes
291292
WORKDIR /config

cps/admin.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2164,6 +2164,7 @@ def _configuration_update_helper():
21642164
_config_checkbox_int(to_save, "config_uploading")
21652165
_config_checkbox_int(to_save, "config_unicode_filename")
21662166
_config_checkbox_int(to_save, "config_embed_metadata")
2167+
_config_checkbox(to_save, "config_fulltext_search")
21672168
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
21682169
reboot_required |= (_config_checkbox_int(to_save, "config_anonbrowse")
21692170
and config.config_login_type == constants.LOGIN_LDAP)
@@ -2309,10 +2310,19 @@ def _configuration_update_helper():
23092310
_configuration_result(_("Oops! Database Error: %(error)s.", error=e.orig))
23102311

23112312
config.save()
2313+
2314+
# Note: FTS initialization is now primarily managed via CWA Settings page
2315+
# This just provides basic validation
2316+
fts_warning = None
2317+
if config.config_fulltext_search and not config.config_calibre_dir:
2318+
fts_warning = _('Full Text Search enabled but no library path configured.')
2319+
23122320
if reboot_required:
23132321
web_server.stop(True)
23142322

2315-
return _configuration_result(None, reboot_required, " ".join(filter(None, [unrar_warning, arch_warning])))
2323+
# Combine all warnings
2324+
combined_warnings = " ".join(filter(None, [unrar_warning, arch_warning, fts_warning]))
2325+
return _configuration_result(None, reboot_required, combined_warnings if combined_warnings else None)
23162326

23172327

23182328
def _configuration_result(error_flash=None, reboot=False, warning_flash=None):

cps/config_sql.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class _Settings(_Base):
146146
config_upload_formats = Column(String, default=','.join(constants.EXTENSIONS_UPLOAD))
147147
config_unicode_filename = Column(Boolean, default=False)
148148
config_embed_metadata = Column(Boolean, default=True)
149+
config_fulltext_search = Column(Boolean, default=False)
149150

150151
config_updatechannel = Column(Integer, default=constants.UPDATE_STABLE)
151152

cps/cwa_functions.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,8 @@ def set_cwa_settings():
711711
int_value = max(500, min(50000, int_value))
712712
elif setting == 'duplicate_scan_debounce_seconds':
713713
int_value = max(5, min(600, int_value))
714+
elif setting == 'duplicate_auto_resolve_cooldown_minutes':
715+
int_value = max(0, min(1440, int_value)) # Clamp between 0 and 1440 minutes (24 hours)
714716
result[setting] = int_value
715717
except (ValueError, TypeError):
716718
# Use current value if conversion fails
@@ -724,6 +726,8 @@ def set_cwa_settings():
724726
result[setting] = cwa_db.cwa_settings.get(setting, 2) # Default to 2 AM
725727
elif setting == 'duplicate_scan_debounce_seconds':
726728
result[setting] = cwa_db.cwa_settings.get(setting, 30)
729+
elif setting == 'duplicate_auto_resolve_cooldown_minutes':
730+
result[setting] = cwa_db.cwa_settings.get(setting, 0) # Default to 0 (disabled)
727731
else:
728732
if setting == 'ingest_timeout_minutes':
729733
result[setting] = cwa_db.cwa_settings.get(setting, 15) # Default to 15 minutes
@@ -735,6 +739,8 @@ def set_cwa_settings():
735739
result[setting] = cwa_db.cwa_settings.get(setting, 2) # Default to 2 AM
736740
elif setting == 'duplicate_scan_debounce_seconds':
737741
result[setting] = cwa_db.cwa_settings.get(setting, 30)
742+
elif setting == 'duplicate_auto_resolve_cooldown_minutes':
743+
result[setting] = cwa_db.cwa_settings.get(setting, 0) # Default to 0 (disabled)
738744

739745
# Handle float settings
740746
for setting in float_settings:
@@ -2041,3 +2047,283 @@ def set_profile_picture():
20412047
return render_title_template("profile_pictures.html",
20422048
title=_("CWA Profile Picture Management (WIP)"),
20432049
page="profile-picture")
2050+
2051+
2052+
# ========================================
2053+
# Full Text Search (FTS) Management
2054+
# ========================================
2055+
2056+
def _get_fts_status_from_calibredb():
2057+
"""
2058+
Get FTS indexing status from calibredb.
2059+
Returns dict with: enabled, indexed, total, percentage, error
2060+
"""
2061+
from .admin import get_calibre_binarypath
2062+
2063+
result = {
2064+
'enabled': False,
2065+
'indexed': 0,
2066+
'total': 0,
2067+
'percentage': 0.0,
2068+
'error': None
2069+
}
2070+
2071+
if not config.config_calibre_dir:
2072+
result['error'] = _('No library path configured')
2073+
return result
2074+
2075+
calibredb_path = get_calibre_binarypath("calibredb")
2076+
if not calibredb_path or not os.path.isfile(calibredb_path):
2077+
result['error'] = _('calibredb not found')
2078+
return result
2079+
2080+
try:
2081+
# Set up environment with LD_LIBRARY_PATH for Calibre libraries
2082+
env = os.environ.copy()
2083+
env['LD_LIBRARY_PATH'] = '/app/calibre/lib'
2084+
2085+
# Run calibredb fts_index status
2086+
cmd = [calibredb_path, 'fts_index', 'status', '--library-path', config.config_calibre_dir]
2087+
proc_result = subprocess.run(
2088+
cmd,
2089+
stdout=subprocess.PIPE,
2090+
stderr=subprocess.PIPE,
2091+
text=True,
2092+
timeout=10,
2093+
env=env
2094+
)
2095+
2096+
# Exit code 0 = enabled, exit code 2 = disabled (both are valid responses)
2097+
if proc_result.returncode in [0, 2]:
2098+
output = proc_result.stdout.strip()
2099+
2100+
# Check if enabled or disabled
2101+
if 'disabled' in output.lower():
2102+
result['enabled'] = False
2103+
# When disabled, there are no books to count
2104+
result['indexed'] = 0
2105+
result['total'] = 0
2106+
result['percentage'] = 0.0
2107+
return result
2108+
elif 'enabled' in output.lower():
2109+
result['enabled'] = True
2110+
2111+
# Parse "X of Y books" pattern
2112+
match = re.search(r'(\d+)\s+of\s+(\d+)\s+books', output)
2113+
if match:
2114+
result['indexed'] = int(match.group(1))
2115+
result['total'] = int(match.group(2))
2116+
if result['total'] > 0:
2117+
result['percentage'] = (result['indexed'] / result['total']) * 100
2118+
else:
2119+
result['error'] = proc_result.stderr.strip() or _('Failed to get FTS status')
2120+
2121+
except subprocess.TimeoutExpired:
2122+
result['error'] = _('Timeout while checking FTS status')
2123+
except Exception as ex:
2124+
result['error'] = str(ex)
2125+
log.error(f"Error getting FTS status: {ex}")
2126+
2127+
return result
2128+
2129+
2130+
def _initialize_fts_index():
2131+
"""
2132+
Initialize or disable Full Text Search indexing based on config.
2133+
Returns a tuple of (success: bool, message: str or None)
2134+
"""
2135+
from .admin import get_calibre_binarypath
2136+
2137+
if not config.config_calibre_dir:
2138+
return False, _('No library path configured')
2139+
2140+
fts_db_path = os.path.join(config.config_calibre_dir, "full-text-search.db")
2141+
2142+
# If FTS disabled but database exists, disable indexing
2143+
if not config.config_fulltext_search and os.path.exists(fts_db_path):
2144+
try:
2145+
calibredb_path = get_calibre_binarypath("calibredb")
2146+
if not calibredb_path or not os.path.isfile(calibredb_path):
2147+
log.warning("calibredb not found, cannot disable FTS indexing")
2148+
return False, _('calibredb not available. Cannot stop FTS indexing.')
2149+
2150+
env = os.environ.copy()
2151+
env['LD_LIBRARY_PATH'] = '/app/calibre/lib'
2152+
2153+
cmd = [calibredb_path, 'fts_index', 'disable', '--library-path', config.config_calibre_dir]
2154+
log.info("Disabling FTS index: %s", ' '.join(cmd))
2155+
2156+
result = subprocess.run(
2157+
cmd,
2158+
input='disable\n', # Must type "disable" to confirm
2159+
stdout=subprocess.PIPE,
2160+
stderr=subprocess.PIPE,
2161+
text=True,
2162+
timeout=30,
2163+
env=env
2164+
)
2165+
2166+
if result.returncode == 0:
2167+
log.info("FTS indexing disabled successfully")
2168+
return True, _('Full Text Search indexing has been disabled and stopped.')
2169+
else:
2170+
log.error("Failed to disable FTS indexing: %s", result.stderr.strip())
2171+
return False, _('Failed to disable FTS indexing: %(error)s', error=result.stderr.strip())
2172+
except Exception as ex:
2173+
log.error("Failed to disable FTS indexing: %s", ex)
2174+
return False, _('Error disabling FTS indexing: %(error)s', error=str(ex))
2175+
2176+
# If FTS not enabled, nothing to do
2177+
if not config.config_fulltext_search:
2178+
return True, None
2179+
2180+
# Check if FTS database already exists AND is enabled
2181+
if os.path.exists(fts_db_path):
2182+
# Check current status to see if it's actually enabled
2183+
current_status = _get_fts_status_from_calibredb()
2184+
if current_status['enabled']:
2185+
log.debug("FTS database already exists and is enabled at %s", fts_db_path)
2186+
return True, None
2187+
# If disabled, fall through to enable it
2188+
2189+
# Try to enable FTS indexing using calibredb
2190+
try:
2191+
calibredb_path = get_calibre_binarypath("calibredb")
2192+
if not calibredb_path or not os.path.isfile(calibredb_path):
2193+
log.warning("calibredb not found, cannot initialize FTS indexing")
2194+
return False, _('Full Text Search enabled but calibredb not available. FTS indexing cannot be initialized.')
2195+
2196+
# Run calibredb fts_index enable
2197+
env = os.environ.copy()
2198+
env['LD_LIBRARY_PATH'] = '/app/calibre/lib'
2199+
2200+
cmd = [calibredb_path, 'fts_index', 'enable', '--library-path', config.config_calibre_dir]
2201+
log.info("Initializing FTS index: %s", ' '.join(cmd))
2202+
2203+
result = subprocess.run(
2204+
cmd,
2205+
stdout=subprocess.PIPE,
2206+
stderr=subprocess.PIPE,
2207+
text=True,
2208+
timeout=30,
2209+
env=env
2210+
)
2211+
2212+
if result.returncode == 0:
2213+
log.info("FTS indexing enabled successfully: %s", result.stdout.strip())
2214+
return True, _('Full Text Search indexing enabled. Books are being indexed in the background.')
2215+
else:
2216+
log.error("Failed to enable FTS indexing: %s", result.stderr.strip())
2217+
return False, _('Failed to enable FTS indexing: %(error)s', error=result.stderr.strip())
2218+
2219+
except Exception as ex:
2220+
log.error("Failed to initialize FTS indexing: %s", ex)
2221+
return False, _('Error initializing FTS indexing: %(error)s', error=str(ex))
2222+
2223+
2224+
@cwa_settings.route('/fts-status', methods=['GET'])
2225+
@login_required_if_no_ano
2226+
@admin_required
2227+
def get_fts_status():
2228+
"""
2229+
Get current FTS indexing status.
2230+
Returns JSON with status information.
2231+
"""
2232+
status = _get_fts_status_from_calibredb()
2233+
2234+
# Determine if search is available (90% threshold)
2235+
status['available'] = status['enabled'] and status['percentage'] >= 90.0
2236+
2237+
# Check if indexing is currently active by comparing two sequential checks
2238+
# If indexed count changes, indexing is active
2239+
if status['enabled'] and status['indexed'] < status['total']:
2240+
sleep(0.5) # Brief pause
2241+
status2 = _get_fts_status_from_calibredb()
2242+
status['active'] = status2['indexed'] > status['indexed']
2243+
else:
2244+
status['active'] = False
2245+
2246+
return jsonify(status)
2247+
2248+
2249+
@cwa_settings.route('/fts-action', methods=['POST'])
2250+
@csrf.exempt
2251+
@login_required_if_no_ano
2252+
@admin_required
2253+
def fts_action():
2254+
"""
2255+
Execute FTS actions: enable, disable, reindex.
2256+
Returns JSON with success status and message.
2257+
"""
2258+
from .admin import get_calibre_binarypath
2259+
2260+
action = request.form.get('action')
2261+
2262+
if action not in ['enable', 'disable', 'reindex']:
2263+
return jsonify({'success': False, 'message': _('Invalid action')}), 400
2264+
2265+
calibredb_path = get_calibre_binarypath("calibredb")
2266+
if not calibredb_path or not os.path.isfile(calibredb_path):
2267+
return jsonify({'success': False, 'message': _('calibredb not available')}), 500
2268+
2269+
if not config.config_calibre_dir:
2270+
return jsonify({'success': False, 'message': _('No library path configured')}), 500
2271+
2272+
try:
2273+
env = os.environ.copy()
2274+
env['LD_LIBRARY_PATH'] = '/app/calibre/lib'
2275+
2276+
if action == 'enable':
2277+
# Update config
2278+
config.config_fulltext_search = True
2279+
config.save()
2280+
2281+
# Initialize FTS
2282+
success, message = _initialize_fts_index()
2283+
if success:
2284+
return jsonify({'success': True, 'message': message or _('FTS indexing enabled successfully')})
2285+
else:
2286+
return jsonify({'success': False, 'message': message}), 500
2287+
2288+
elif action == 'disable':
2289+
# Update config
2290+
try:
2291+
config.config_fulltext_search = False
2292+
config.save()
2293+
log.info("FTS config setting disabled and saved")
2294+
except Exception as save_ex:
2295+
log.error(f"Error saving config during FTS disable: {save_ex}")
2296+
return jsonify({'success': False, 'message': f'Failed to save config: {str(save_ex)}'}), 500
2297+
2298+
# Disable FTS
2299+
success, message = _initialize_fts_index()
2300+
if success:
2301+
return jsonify({'success': True, 'message': message or _('FTS indexing disabled successfully')})
2302+
else:
2303+
return jsonify({'success': False, 'message': message}), 500
2304+
2305+
elif action == 'reindex':
2306+
if not config.config_fulltext_search:
2307+
return jsonify({'success': False, 'message': _('FTS must be enabled before reindexing')}), 400
2308+
2309+
cmd = [calibredb_path, 'fts_index', 'reindex', '--library-path', config.config_calibre_dir]
2310+
log.info("Reindexing FTS: %s", ' '.join(cmd))
2311+
2312+
result = subprocess.run(
2313+
cmd,
2314+
input='reindex\n', # Must type "reindex" to confirm
2315+
stdout=subprocess.PIPE,
2316+
stderr=subprocess.PIPE,
2317+
text=True,
2318+
timeout=30,
2319+
env=env
2320+
)
2321+
2322+
if result.returncode == 0:
2323+
return jsonify({'success': True, 'message': _('FTS reindexing started successfully')})
2324+
else:
2325+
return jsonify({'success': False, 'message': result.stderr.strip()}), 500
2326+
2327+
except Exception as ex:
2328+
log.error(f"FTS action failed: {ex}")
2329+
return jsonify({'success': False, 'message': str(ex)}), 500

0 commit comments

Comments
 (0)