diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md
index baec1fa44d26..d913a23dbab1 100644
--- a/docs/docs/documentation/getting-started/installation/backend-config.md
+++ b/docs/docs/documentation/getting-started/installation/backend-config.md
@@ -16,7 +16,8 @@
| TZ | UTC | Must be set to get correct date/time on the server |
| ALLOW_SIGNUP\* | false | Allow user sign-up without token |
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
-| LOG_LEVEL | info | logging level configured |
+| LOG_LEVEL | info | Logging level configured |
+| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run the daily tasks. |
\* Starting in v1.4.0 this was changed to default to `false` as apart of a security review of the application.
diff --git a/mealie/core/settings/settings.py b/mealie/core/settings/settings.py
index 8d6c6afea2c7..f966729bced6 100644
--- a/mealie/core/settings/settings.py
+++ b/mealie/core/settings/settings.py
@@ -57,6 +57,8 @@ class AppSettings(BaseSettings):
ALLOW_SIGNUP: bool = False
+ DAILY_SCHEDULE_TIME: str = "23:45"
+
# ===============================================
# Security Configuration
@@ -199,7 +201,11 @@ class AppSettings(BaseSettings):
def OIDC_READY(self) -> bool:
"""Validates OIDC settings are all set"""
- required = {self.OIDC_CLIENT_ID, self.OIDC_CONFIGURATION_URL, self.OIDC_USER_CLAIM}
+ required = {
+ self.OIDC_CLIENT_ID,
+ self.OIDC_CONFIGURATION_URL,
+ self.OIDC_USER_CLAIM,
+ }
not_none = None not in required
valid_group_claim = True
if (not self.OIDC_USER_GROUP or not self.OIDC_ADMIN_GROUP) and not self.OIDC_GROUPS_CLAIM:
diff --git a/mealie/services/scheduler/scheduler_service.py b/mealie/services/scheduler/scheduler_service.py
index 8a930bc0637c..0f121ccf1338 100644
--- a/mealie/services/scheduler/scheduler_service.py
+++ b/mealie/services/scheduler/scheduler_service.py
@@ -1,6 +1,9 @@
+import asyncio
+from datetime import datetime, timedelta
from pathlib import Path
from mealie.core import root_logger
+from mealie.core.config import get_app_settings
from mealie.services.scheduler.runner import repeat_every
from .scheduler_registry import SchedulerRegistry
@@ -18,18 +21,53 @@ class SchedulerService:
@staticmethod
async def start():
await run_minutely()
- await run_daily()
await run_hourly()
+ # Wait to trigger our daily run until our given "daily time", so having asyncio handle it.
+ asyncio.create_task(schedule_daily())
+
+
+async def schedule_daily():
+ now = datetime.now()
+ daily_schedule_time = get_app_settings().DAILY_SCHEDULE_TIME
+ logger.debug(
+ "Current time is %s and DAILY_SCHEDULE_TIME is %s",
+ str(now),
+ daily_schedule_time,
+ )
+ try:
+ hour_target, minute_target = _parse_daily_schedule_time(daily_schedule_time)
+ except Exception:
+ logger.exception(f"Unable to parse {daily_schedule_time=}")
+ hour_target = 23
+ minute_target = 45
+
+ hours_until = ((hour_target - now.hour) % 24) or 24
+ minutes_until = (minute_target - now.minute) % 60
+ logger.debug("Hours until %s and minutes until %s", str(hours_until), str(minutes_until))
+
+ delta = timedelta(hours=hours_until, minutes=minutes_until)
+ target_time = (now + delta).replace(microsecond=0, second=0)
+ logger.info("Daily tasks scheduled for %s", str(target_time))
+ wait_seconds = (target_time - now).total_seconds()
+ await asyncio.sleep(wait_seconds)
+ await run_daily()
+
+
+def _parse_daily_schedule_time(time):
+ hour_target = int(time.split(":")[0])
+ minute_target = int(time.split(":")[1])
+ return hour_target, minute_target
+
def _scheduled_task_wrapper(callable):
try:
callable()
except Exception as e:
- logger.error(f"Error in scheduled task func='{callable.__name__}': exception='{e}'")
+ logger.error("Error in scheduled task func='%s': exception='%s'", callable.__name__, e)
-@repeat_every(minutes=MINUTES_DAY, wait_first=True, logger=logger)
+@repeat_every(minutes=MINUTES_DAY, wait_first=False, logger=logger)
def run_daily():
logger.debug("Running daily callbacks")
for func in SchedulerRegistry._daily: