diff --git a/Makefile b/Makefile
index 20e8ff58..2f1d995e 100644
--- a/Makefile
+++ b/Makefile
@@ -114,6 +114,7 @@ SRCS = src/version.c \
src/input.c \
src/http/http_client.c \
src/fsmonitor.c \
+ src/cron.c \
SRCS += \
src/api.c \
diff --git a/src/cron.c b/src/cron.c
new file mode 100644
index 00000000..0dbcbbfd
--- /dev/null
+++ b/src/cron.c
@@ -0,0 +1,329 @@
+/*
+ * Tvheadend - cron routines
+ *
+ * Copyright (C) 2014 Adam Sutton
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "cron.h"
+
+#include
+#include
+#include
+
+/*
+ * Parse value
+ */
+static int
+cron_parse_val ( const char *str, const char **key, int *v )
+{
+ int i = 0;
+ if (key) {
+ while (key[i]) {
+ if (!strncasecmp(str, key[i], strlen(key[i]))) {
+ *v = i;
+ return 0;
+ }
+ i++;
+ }
+ }
+
+ return sscanf(str, "%d", v) == 1 ? 0 : 1;
+}
+
+/*
+ * Parse individual field in cron spec
+ */
+static int
+cron_parse_field
+ ( const char **istr, uint64_t *field, uint64_t mask, int bits, int off,
+ const char **key )
+{
+ int sn = -1, en = -1, mn = -1;
+ const char *str = *istr;
+ const char *beg = str;
+ uint64_t val = 0;
+ while ( 1 ) {
+ if ( *str == '*' ) {
+ sn = 0;
+ en = bits - 1;
+ beg = NULL;
+ } else if ( *str == ',' || *str == ' ' || *str == '\0' ) {
+ if (beg)
+ if (cron_parse_val(beg, key, en == -1 ? (sn == -1 ? &sn : &en) : &mn))
+ return 1;
+ if ((sn - off) >= bits || (en - off) >= bits || mn > bits)
+ return 1;
+ if (en < 0) en = sn;
+ if (mn < 0) mn = 1;
+ while (sn <= en) {
+ if ( (sn % mn) == 0 )
+ val |= (0x1LL << (sn - off));
+ sn++;
+ }
+ if (*str != ',') break;
+ sn = en = mn = -1;
+ beg = (str + 1);
+ } else if ( *str == '/' ) {
+ if (beg)
+ if (en == -1 || cron_parse_val(beg, key, sn == -1 ? &sn : &en))
+ return 1;
+ beg = (str + 1);
+ } else if ( *str == '-' ) {
+ if (sn != -1 || cron_parse_val(beg, key, &sn))
+ return 1;
+ beg = (str + 1);
+ }
+ str++;
+ }
+ if (*str == ' ') str++;
+ *istr = str;
+ *field = (val | ((val >> bits) & 0x1)) & mask;
+ return 0;
+}
+
+/*
+ * Set value
+ */
+int
+cron_set ( cron_t *c, const char *str )
+{
+ uint64_t ho, mi, mo, dm, dw;
+ static const char *days[] = {
+ "sun", "mon", "tue", "wed", "thu", "fri", "sat"
+ };
+ static const char *months[] = {
+ "ignore",
+ "jan", "feb", "mar", "apr", "may", "jun",
+ "jul", "aug", "sep", "oct", "nov", "dec"
+ };
+
+ /* Daily (01:01) */
+ if ( !strcmp(str, "@daily") ) {
+ c->c_min = 1;
+ c->c_hour = 1;
+ c->c_mday = CRON_MDAY_MASK;
+ c->c_mon = CRON_MON_MASK;
+ c->c_wday = CRON_WDAY_MASK;
+
+ /* Hourly (XX:02) */
+ } else if ( !strcmp(str, "@hourly") ) {
+ c->c_min = 2;
+ c->c_hour = CRON_HOUR_MASK;
+ c->c_mday = CRON_MDAY_MASK;
+ c->c_mon = CRON_MON_MASK;
+ c->c_wday = CRON_WDAY_MASK;
+
+ /* Standard */
+ } else {
+ if (cron_parse_field(&str, &mi, CRON_MIN_MASK, 60, 0, NULL) || !mi)
+ return 1;
+ if (cron_parse_field(&str, &ho, CRON_HOUR_MASK, 24, 0, NULL) || !ho)
+ return 1;
+ if (cron_parse_field(&str, &dm, CRON_MDAY_MASK, 31, 1, NULL) || !dm)
+ return 1;
+ if (cron_parse_field(&str, &mo, CRON_MON_MASK, 12, 1, months) || !mo)
+ return 1;
+ if (cron_parse_field(&str, &dw, CRON_WDAY_MASK, 7, 0, days) || !dw)
+ return 1;
+ c->c_min = mi;
+ c->c_hour = ho;
+ c->c_mday = dm;
+ c->c_mon = mo;
+ c->c_wday = dw;
+ }
+
+ return 0;
+}
+
+/*
+ * Check for leap year
+ */
+static int
+is_leep_year ( int year )
+{
+ if (!(year % 400))
+ return 1;
+ if (!(year % 100))
+ return 0;
+ return (year % 4) ? 0 : 1;
+}
+
+/*
+ * Check for days in month
+ */
+static int
+days_in_month ( int year, int mon )
+{
+ int d;
+ if (mon == 2)
+ d = 28 + is_leep_year(year);
+ else
+ d = 30 + ((0x15AA >> mon) & 0x1);
+ return d;
+}
+
+/*
+ * Find the next time (starting from now) that the cron should fire
+ */
+int
+cron_next ( cron_t *c, const time_t now, time_t *ret )
+{
+ struct tm nxt;
+ int endyear;
+ localtime_r(&now, &nxt);
+ endyear = nxt.tm_year + 10;
+
+ /* Invalid day */
+ if (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) ||
+ !(c->c_wday & (0x1LL << (nxt.tm_wday))) ||
+ !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) {
+ nxt.tm_min = 0;
+ nxt.tm_hour = 0;
+
+ /* Invalid hour */
+ } else if (!(c->c_hour & (0x1LL << nxt.tm_hour))) {
+ nxt.tm_min = 0;
+
+ /* Increment */
+ } else {
+ ++nxt.tm_min;
+ }
+
+ /* Minute */
+ while (!(c->c_min & (0x1LL << nxt.tm_min))) {
+ if (nxt.tm_min == 60) {
+ ++nxt.tm_hour;
+ nxt.tm_min = 0;
+ } else
+ nxt.tm_min++;
+ }
+
+ /* Hour */
+ while (!(c->c_hour & (0x1LL << nxt.tm_hour))) {
+ if (nxt.tm_hour == 24) {
+ ++nxt.tm_mday;
+ ++nxt.tm_wday;
+ nxt.tm_hour = 0;
+ } else
+ ++nxt.tm_hour;
+ }
+
+ /* Date */
+ if (nxt.tm_wday == 7)
+ nxt.tm_wday = 0;
+ if (nxt.tm_mday == days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) {
+ nxt.tm_mday = 1;
+ nxt.tm_mon++;
+ if (nxt.tm_mon == 12) {
+ nxt.tm_mon = 0;
+ ++nxt.tm_year;
+ }
+ }
+ while (!(c->c_mday & (0x1LL << (nxt.tm_mday-1))) ||
+ !(c->c_wday & (0x1LL << (nxt.tm_wday))) ||
+ !(c->c_mon & (0x1LL << (nxt.tm_mon))) ) {
+
+ /* Stop possible infinite loop on invalid request */
+ if (nxt.tm_year >= endyear)
+ return -1;
+
+ /* Increment day of week */
+ if (++nxt.tm_wday == 7)
+ nxt.tm_wday = 0;
+
+ /* Increment day */
+ if (++nxt.tm_mday > days_in_month(nxt.tm_year+1900, nxt.tm_mon+1)) {
+ nxt.tm_mday = 1;
+ if (++nxt.tm_mon == 12) {
+ nxt.tm_mon = 0;
+ ++nxt.tm_year;
+ }
+ }
+
+ /* Shortcut the month */
+ while (!(c->c_mon & (0x1LL << nxt.tm_mon))) {
+ nxt.tm_wday
+ += 1 + (days_in_month(nxt.tm_year+1900, nxt.tm_mon+1) - nxt.tm_mday);
+ nxt.tm_mday = 1;
+ if (++nxt.tm_mon >= 12) {
+ nxt.tm_mon = 0;
+ ++nxt.tm_year;
+ }
+ }
+ nxt.tm_wday %= 7;
+ }
+
+ /* Create time */
+ // TODO: not sure this will provide the correct time with respect to DST!
+ nxt.tm_isdst = 0;
+ *ret = mktime(&nxt);
+ return 0;
+}
+
+/*
+ * Testing
+ */
+#if 0
+static
+void print_bits ( uint64_t b, int n )
+{
+ while (n) {
+ printf("%d", (int)(b & 0x1));
+ b >>= 1;
+ n--;
+ }
+}
+
+int
+main ( int argc, char **argv )
+{
+ cron_t c;
+ time_t n;
+ struct tm tm;
+ char buf[128];
+
+ time(&n);
+ if (cron_set(&c, argv[1]))
+ printf("INVALID CRON: %s\n", argv[1]);
+ else {
+ printf("min = "); print_bits(c.c_min, 60); printf("\n");
+ printf("hour = "); print_bits(c.c_hour, 24); printf("\n");
+ printf("mday = "); print_bits(c.c_mday, 31); printf("\n");
+ printf("mon = "); print_bits(c.c_mon, 12); printf("\n");
+ printf("wday = "); print_bits(c.c_wday, 7); printf("\n");
+
+ localtime_r(&n, &tm);
+ strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm);
+ printf("NOW: %s\n", buf);
+
+ if (cron_next(&c, n, &n)) {
+ printf("FAILED to find NEXT\n");
+ return 1;
+ }
+ localtime_r(&n, &tm);
+ strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &tm);
+ printf("NXT: %s\n", buf);
+
+ }
+ return 0;
+}
+#endif
+
+/******************************************************************************
+ * Editor Configuration
+ *
+ * vim:sts=2:ts=2:sw=2:et
+ *****************************************************************************/
diff --git a/src/cron.h b/src/cron.h
new file mode 100644
index 00000000..155afa17
--- /dev/null
+++ b/src/cron.h
@@ -0,0 +1,68 @@
+/*
+ * Tvheadend - cron routines
+ *
+ * Copyright (C) 2014 Adam Sutton
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef __TVH_CRON_H__
+#define __TVH_CRON_H__
+
+#include
+#include
+
+#define CRON_MIN_MASK (0x0FFFFFFFFFFFFFFFLL) // 60 bits
+#define CRON_HOUR_MASK (0x00FFFFFF) // 24 bits
+#define CRON_MDAY_MASK (0x7FFFFFFF) // 31 bits
+#define CRON_MON_MASK (0x0FFF) // 12 bits
+#define CRON_WDAY_MASK (0x7F) // 7 bits
+
+typedef struct cron
+{
+ uint64_t c_min; ///< Minute mask
+ uint32_t c_hour; ///< Hour mask
+ uint32_t c_mday; ///< Day of the Month mask
+ uint16_t c_mon; ///< Month mask
+ uint8_t c_wday; ///< Day of the Week mask
+} cron_t;
+
+/**
+ * Initialise from a string
+ *
+ * @param c The cron instance to update
+ * @param str String representation of the cron
+ *
+ * @return 0 if OK, 1 if failed to parse
+ */
+int cron_set ( cron_t *c, const char *str );
+
+/**
+ * Determine the next time a cron will run (from cur)
+ *
+ * @param c The cron to check
+ * @param now The current time
+ * @param nxt The next time to execute
+ *
+ * @return 0 if next time was found
+ */
+int cron_next ( cron_t *c, const time_t cur, time_t *nxt );
+
+#endif /* __TVH_CRON_H__ */
+
+/******************************************************************************
+ * Editor Configuration
+ *
+ * vim:sts=2:ts=2:sw=2:et
+ *****************************************************************************/