Skip to content

Commit 689d223

Browse files
feat(duplicates): finalize Phase 2 scanning with hybrid detection, background tasking, cron scheduling, and UI feedback
default hybrid detection + SQL prefilter; updated defaults in schema duplicate scans now run as background tasks with progress + cancel debounced after-import scans and cron-based scheduled scans settings UI updated for cron, defaults, and explanations + next run display duplicates page shows progress bar + next scheduled run task queue fixes + better error/confirmation messaging cron validation on save and ISO task date formatting
1 parent ce3c452 commit 689d223

13 files changed

Lines changed: 616 additions & 45 deletions

cps/cwa_functions.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def set_cwa_settings():
554554
boolean_settings = []
555555
string_settings = []
556556
list_settings = []
557-
integer_settings = ['ingest_timeout_minutes', 'auto_send_delay_minutes'] # Special handling for integer settings
557+
integer_settings = ['ingest_timeout_minutes', 'auto_send_delay_minutes', 'duplicate_scan_hour', 'duplicate_scan_chunk_size'] # Special handling for integer settings
558558
json_settings = ['metadata_provider_hierarchy', 'metadata_providers_enabled', 'duplicate_format_priority'] # Special handling for JSON settings
559559

560560
for setting in cwa_default_settings:
@@ -567,6 +567,10 @@ def set_cwa_settings():
567567
else:
568568
list_settings.append(setting)
569569

570+
# Ensure cron expression is treated as a string even if default is empty
571+
if 'duplicate_scan_cron' not in string_settings:
572+
string_settings.append('duplicate_scan_cron')
573+
570574
for format in ignorable_formats:
571575
string_settings.append(f"ignore_ingest_{format}")
572576
string_settings.append(f"ignore_convert_{format}")
@@ -631,6 +635,10 @@ def set_cwa_settings():
631635
int_value = max(5, min(120, int_value)) # Clamp between 5 and 120 minutes
632636
elif setting == 'auto_send_delay_minutes':
633637
int_value = max(1, min(60, int_value)) # Clamp between 1 and 60 minutes
638+
elif setting == 'duplicate_scan_hour':
639+
int_value = max(0, min(23, int_value))
640+
elif setting == 'duplicate_scan_chunk_size':
641+
int_value = max(500, min(50000, int_value))
634642
result[setting] = int_value
635643
except (ValueError, TypeError):
636644
# Use current value if conversion fails
@@ -689,6 +697,17 @@ def set_cwa_settings():
689697
else:
690698
result[setting] = cwa_db.cwa_settings.get(setting, '[]')
691699

700+
# Validate cron expression if provided
701+
cron_expr = result.get('duplicate_scan_cron', '')
702+
if cron_expr:
703+
try:
704+
from apscheduler.triggers.cron import CronTrigger
705+
CronTrigger.from_crontab(cron_expr)
706+
except Exception:
707+
# Revert to previous value and notify user
708+
result['duplicate_scan_cron'] = cwa_db.cwa_settings.get('duplicate_scan_cron', '')
709+
flash(_("Invalid cron expression for duplicate scans. Changes were not saved."), category="error")
710+
692711
# DEBUGGING
693712
# with open("/config/post_request" ,"w") as f:
694713
# for key in result.keys():
@@ -713,9 +732,33 @@ def set_cwa_settings():
713732
cwa_db = CWA_DB()
714733
cwa_settings = cwa_db.get_cwa_settings()
715734

735+
next_scan_run = get_next_duplicate_scan_run(cwa_settings)
736+
716737
return render_title_template("cwa_settings.html", title=_("Calibre-Web Automated User Settings"), page="cwa-settings",
717738
cwa_settings=cwa_settings, ignorable_formats=ignorable_formats, target_formats=target_formats,
718-
automerge_options=automerge_options, autoingest_options=autoingest_options)
739+
automerge_options=automerge_options, autoingest_options=autoingest_options,
740+
next_duplicate_scan_run=next_scan_run)
741+
742+
743+
def get_next_duplicate_scan_run(settings):
744+
"""Compute next scheduled duplicate scan run time based on settings."""
745+
try:
746+
enabled = bool(settings.get('duplicate_scan_enabled', 0))
747+
cron_expr = (settings.get('duplicate_scan_cron') or '').strip()
748+
749+
if not enabled:
750+
return None
751+
752+
if not cron_expr:
753+
return None
754+
755+
from apscheduler.triggers.cron import CronTrigger
756+
now = datetime.now().astimezone()
757+
trigger = CronTrigger.from_crontab(cron_expr, timezone=now.tzinfo)
758+
next_run = trigger.get_next_fire_time(None, now)
759+
return next_run.isoformat() if next_run else None
760+
except Exception:
761+
return None
719762

720763
##————————————————————————————————————————————————————————————————————————————##
721764
## ##

0 commit comments

Comments
 (0)