diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py
index 460e1d9fdb..1f6a394556 100644
--- a/src/calibre/gui2/dialogs/scheduler.py
+++ b/src/calibre/gui2/dialogs/scheduler.py
@@ -8,9 +8,11 @@ Scheduler for automated recipe downloads
'''
from datetime import timedelta
+import calendar, textwrap
-from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
- QAction, QIcon, QMutex, QTimer, pyqtSignal
+from PyQt4.Qt import QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout, \
+ QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout, \
+ QCheckBox, QTimeEdit, QLabel, QLineEdit, QDoubleSpinBox
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
from calibre.gui2 import config as gconf, error_dialog
@@ -18,9 +20,171 @@ from calibre.web.feeds.recipes.model import RecipeModel
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import utcnow
from calibre.utils.network import internet_connected
+from calibre.utils.ordered_dict import OrderedDict
+
+def convert_day_time_schedule(val):
+ day_of_week, hour, minute = val
+ if day_of_week == -1:
+ return (tuple(xrange(7)), hour, minute)
+ return ((day_of_week,), hour, minute)
+
+class Base(QWidget):
+
+ def __init__(self, parent=None):
+ QWidget.__init__(self, parent)
+ self.l = QGridLayout()
+ self.setLayout(self.l)
+ self.setToolTip(textwrap.dedent(self.HELP))
+
+class DaysOfWeek(Base):
+
+ HELP = _('''\
+ Download this periodical every week on the specified days after
+ the specified time. For example, if you choose: Monday after
+ 9:00 AM, then the periodical will be download every Monday as
+ soon after 9:00 AM as possible.
+ ''')
+
+ def __init__(self, parent=None):
+ Base.__init__(self, parent)
+ self.days = [QCheckBox(calendar.day_abbr[d], self) for d in xrange(7)]
+ for i, cb in enumerate(self.days):
+ row = i % 2
+ col = i // 2
+ self.l.addWidget(cb, row, col, 1, 1)
+
+ self.time = QTimeEdit(self)
+ self.time.setDisplayFormat('hh:mm AP')
+ self.hl = QHBoxLayout()
+ self.l1 = QLabel(_('&Download after:'))
+ self.l1.setBuddy(self.time)
+ self.hl.addWidget(self.l1)
+ self.hl.addWidget(self.time)
+ self.l.addLayout(self.hl, 1, 3, 1, 1)
+ self.initialize()
+
+ def initialize(self, typ=None, val=None):
+ if typ is None:
+ typ = 'day/time'
+ val = (-1, 9, 0)
+ if typ == 'day/time':
+ val = convert_day_time_schedule(val)
+
+ days_of_week, hour, minute = val
+ for i, d in enumerate(self.days):
+ d.setChecked(i in days_of_week)
+
+ self.time.setTime(QTime(hour, minute))
+
+ @property
+ def schedule(self):
+ days_of_week = tuple([i for i, d in enumerate(self.days) if
+ d.isChecked()])
+ t = self.time.time()
+ hour, minute = t.hour(), t.minute()
+ return 'days_of_week', (days_of_week, int(hour), int(minute))
+
+class DaysOfMonth(Base):
+
+ HELP = _('''\
+ Download this periodical every month, on the specified days.
+ The download will happen as soon after the specified time as
+ possible on the specified days of each month. For example,
+ if you choose the 1st and the 15th after 9:00 AM, the
+ periodical will be downloaded on the 1st and 15th of every
+ month, as soon after 9:00 AM as possible.
+ ''')
+
+ def __init__(self, parent=None):
+ Base.__init__(self, parent)
+
+ self.l1 = QLabel(_('&Days of the month:'))
+ self.days = QLineEdit(self)
+ self.days.setToolTip(_('Comma separated list of days of the month.'
+ ' For example: 1, 15'))
+ self.l1.setBuddy(self.days)
+
+ self.l2 = QLabel(_('Download &after:'))
+ self.time = QTimeEdit(self)
+ self.time.setDisplayFormat('hh:mm AP')
+ self.l2.setBuddy(self.time)
+
+ self.l.addWidget(self.l1, 0, 0, 1, 1)
+ self.l.addWidget(self.days, 0, 1, 1, 1)
+ self.l.addWidget(self.l2, 1, 0, 1, 1)
+ self.l.addWidget(self.time, 1, 1, 1, 1)
+
+ def initialize(self, typ=None, val=None):
+ if val is None:
+ val = ((1,), 9, 0)
+ days_of_month, hour, minute = val
+ self.days.setText(', '.join(map(str, map(int, days_of_month))))
+ self.time.setTime(QTime(hour, minute))
+
+ @property
+ def schedule(self):
+ parts = [x.strip() for x in unicode(self.days.text()).split(',') if
+ x.strip()]
+ try:
+ days_of_month = tuple(map(int, parts))
+ except:
+ days_of_month = (1,)
+ if not days_of_month:
+ days_of_month = (1,)
+ t = self.time.time()
+ hour, minute = t.hour(), t.minute()
+ return 'days_of_month', (days_of_month, int(hour), int(minute))
+
+class EveryXDays(Base):
+
+ HELP = _('''\
+ Download this periodical every x days. For example, if you
+ choose 30 days, the periodical will be downloaded every 30
+ days. Note that you can set periods of less than a day, like
+ 0.1 days to download a periodical more than once a day.
+ ''')
+
+ def __init__(self, parent=None):
+ Base.__init__(self, parent)
+ self.l1 = QLabel(_('&Download every:'))
+ self.interval = QDoubleSpinBox(self)
+ self.interval.setMinimum(0.04)
+ self.interval.setSpecialValueText(_('every hour'))
+ self.interval.setMaximum(1000.0)
+ self.interval.setValue(31.0)
+ self.interval.setSuffix(' ' + _('days'))
+ self.interval.setSingleStep(1.0)
+ self.interval.setDecimals(2)
+ self.l1.setBuddy(self.interval)
+ self.l2 = QLabel(_('Note: You can set intervals of less than a day,'
+ ' by typing the value manually.'))
+ self.l2.setWordWrap(True)
+
+ self.l.addWidget(self.l1, 0, 0, 1, 1)
+ self.l.addWidget(self.interval, 0, 1, 1, 1)
+ self.l.addWidget(self.l2, 1, 0, 1, -1)
+
+ def initialize(self, typ=None, val=None):
+ if val is None:
+ val = 31.0
+ self.interval.setValue(val)
+
+ @property
+ def schedule(self):
+ schedule = self.interval.value()
+ return 'interval', schedule
+
class SchedulerDialog(QDialog, Ui_Dialog):
+ SCHEDULE_TYPES = OrderedDict([
+ ('days_of_week', DaysOfWeek),
+ ('days_of_month', DaysOfMonth),
+ ('every_x_days', EveryXDays),
+ ])
+
+ download = pyqtSignal(object)
+
def __init__(self, recipe_model, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
@@ -30,6 +194,11 @@ class SchedulerDialog(QDialog, Ui_Dialog):
_('%s news sources') %
self.recipe_model.showing_count)
+ self.schedule_widgets = []
+ for key in reversed(self.SCHEDULE_TYPES):
+ self.schedule_widgets.insert(0, self.SCHEDULE_TYPES[key](self))
+ self.schedule_stack.insertWidget(0, self.schedule_widgets[0])
+
self.search.initialize('scheduler_search_history')
self.search.setMinimumContentsLength(15)
self.search.search.connect(self.recipe_model.search)
@@ -43,29 +212,43 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.detail_box.setVisible(False)
self.download_button.setVisible(False)
self.recipes.currentChanged = self.current_changed
- self.interval_button.setChecked(True)
+ for b, c in self.SCHEDULE_TYPES.iteritems():
+ b = getattr(self, b)
+ b.toggled.connect(self.schedule_type_selected)
+ b.setToolTip(textwrap.dedent(c.HELP))
+ self.days_of_week.setChecked(True)
- self.connect(self.schedule, SIGNAL('stateChanged(int)'),
- self.toggle_schedule_info)
- self.connect(self.show_password, SIGNAL('stateChanged(int)'),
- lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password))
- self.connect(self.download_button, SIGNAL('clicked()'),
- self.download_clicked)
- self.connect(self.download_all_button, SIGNAL('clicked()'),
+ self.schedule.stateChanged[int].connect(self.toggle_schedule_info)
+ self.show_password.stateChanged[int].connect(self.set_pw_echo_mode)
+ self.download_button.clicked.connect(self.download_clicked)
+ self.download_all_button.clicked.connect(
self.download_all_clicked)
self.old_news.setValue(gconf['oldest_news'])
+ def set_pw_echo_mode(self, state):
+ self.password.setEchoMode(self.password.Normal
+ if state == Qt.Checked else self.password.Password)
+
+
+ def schedule_type_selected(self, *args):
+ for i, st in enumerate(self.SCHEDULE_TYPES):
+ if getattr(self, st).isChecked():
+ self.schedule_stack.setCurrentIndex(i)
+ break
+
def keyPressEvent(self, ev):
if ev.key() not in (Qt.Key_Enter, Qt.Key_Return):
return QDialog.keyPressEvent(self, ev)
def break_cycles(self):
- self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
- self.search_done)
- self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
- self.search.search_done)
- self.search.search.disconnect()
+ try:
+ self.recipe_model.searched.disconnect(self.search_done)
+ self.recipe_model.searched.disconnect(self.search.search_done)
+ self.search.search.disconnect()
+ self.download.disconnect()
+ except:
+ pass
self.recipe_model = None
def search_done(self, *args):
@@ -74,8 +257,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
def toggle_schedule_info(self, *args):
enabled = self.schedule.isChecked()
- for x in ('daily_button', 'day', 'time', 'interval_button', 'interval'):
+ for x in self.SCHEDULE_TYPES:
getattr(self, x).setEnabled(enabled)
+ self.schedule_stack.setEnabled(enabled)
self.last_downloaded.setVisible(enabled)
def current_changed(self, current, previous):
@@ -97,14 +281,14 @@ class SchedulerDialog(QDialog, Ui_Dialog):
return False
return QDialog.accept(self)
- def download_clicked(self):
+ def download_clicked(self, *args):
self.commit()
if self.commit() and self.current_urn:
- self.emit(SIGNAL('download(PyQt_PyObject)'), self.current_urn)
+ self.download.emit(self.current_urn)
- def download_all_clicked(self):
+ def download_all_clicked(self, *args):
if self.commit() and self.commit():
- self.emit(SIGNAL('download(PyQt_PyObject)'), None)
+ self.download.emit(None)
@property
def current_urn(self):
@@ -130,16 +314,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.set_account_info(urn, un, pw)
if self.schedule.isChecked():
- schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time'
- if schedule_type == 'interval':
- schedule = self.interval.value()
- if schedule < 0.1:
- schedule = 1./24.
- else:
- day_of_week = self.day.currentIndex() - 1
- t = self.time.time()
- hour, minute = t.hour(), t.minute()
- schedule = (day_of_week, hour, minute)
+ schedule_type, schedule = \
+ self.schedule_stack.currentWidget().schedule
self.recipe_model.schedule_recipe(urn, schedule_type, schedule)
else:
self.recipe_model.un_schedule_recipe(urn)
@@ -192,27 +368,27 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.schedule.setChecked(scheduled)
self.toggle_schedule_info()
self.last_downloaded.setText(_('Last downloaded: never'))
+ ld_text = _('never')
if scheduled:
typ, sch, last_downloaded = schedule_info
- if typ == 'interval':
- self.interval_button.setChecked(True)
- self.interval.setValue(sch)
- elif typ == 'day/time':
- self.daily_button.setChecked(True)
- day, hour, minute = sch
- self.day.setCurrentIndex(day+1)
- self.time.setTime(QTime(hour, minute))
-
d = utcnow() - last_downloaded
def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60
hours, minutes = hm(d.seconds)
tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes)
if d < timedelta(days=366):
- self.last_downloaded.setText(_('Last downloaded')+': '+tm)
-
+ ld_text = tm
+ else:
+ typ, sch = 'day/time', (-1, 9, 0)
+ sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
+ 'interval':2}[typ]
+ rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
+ rb.setChecked(True)
+ self.schedule_stack.setCurrentIndex(sch_widget)
+ self.schedule_stack.currentWidget().initialize(typ, sch)
add_title_tag, custom_tags, keep_issues = customize_info
self.add_title_tag.setChecked(add_title_tag)
self.custom_tags.setText(u', '.join(custom_tags))
+ self.last_downloaded.setText(_('Last downloaded:') + ' ' + ld_text)
try:
keep_issues = int(keep_issues)
except:
@@ -241,9 +417,9 @@ class Scheduler(QObject):
self.news_icon = QIcon(I('news.png'))
self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self)
self.news_menu.addAction(self.scheduler_action)
- self.connect(self.scheduler_action, SIGNAL('triggered(bool)'), self.show_dialog)
+ self.scheduler_action.triggered[bool].connect(self.show_dialog)
self.cac = QAction(QIcon(I('user_profile.png')), _('Add a custom news source'), self)
- self.connect(self.cac, SIGNAL('triggered(bool)'), self.customize_feeds)
+ self.cac.triggered[bool].connect(self.customize_feeds)
self.news_menu.addAction(self.cac)
self.news_menu.addSeparator()
self.all_action = self.news_menu.addAction(
@@ -252,7 +428,7 @@ class Scheduler(QObject):
self.timer = QTimer(self)
self.timer.start(int(self.INTERVAL * 60 * 1000))
- self.connect(self.timer, SIGNAL('timeout()'), self.check)
+ self.timer.timeout.connect(self.check)
self.oldest = gconf['oldest_news']
QTimer.singleShot(5 * 1000, self.oldest_check)
self.database_changed = self.recipe_model.database_changed
@@ -276,8 +452,7 @@ class Scheduler(QObject):
self.lock.lock()
try:
d = SchedulerDialog(self.recipe_model)
- self.connect(d, SIGNAL('download(PyQt_PyObject)'),
- self.download_clicked)
+ d.download.connect(self.download_clicked)
d.exec_()
gconf['oldest_news'] = self.oldest = d.old_news.value()
d.break_cycles()
diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui
index 26953bbe16..f295703b33 100644
--- a/src/calibre/gui2/dialogs/scheduler.ui
+++ b/src/calibre/gui2/dialogs/scheduler.ui
@@ -61,7 +61,7 @@
&Schedule
-
+
-
@@ -76,141 +76,94 @@
-
-
-
- &Schedule for download:
+
+
+ Qt::Vertical
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ &Schedule for download:
+
+
+
+ -
+
+
-
+
+
+ Days of week
+
+
+
+ -
+
+
+ Days of month
+
+
+
+ -
+
+
+ Every x days
+
+
+
+
+
+ -
+
+
+
+ 16777215
+ 75
+
+
+
+
+ -
+
+
+
+
+
+ true
+
+
+
+
-
-
-
-
-
-
- Every
-
-
-
- -
-
-
-
-
- day
-
-
- -
-
- Monday
-
-
- -
-
- Tuesday
-
-
- -
-
- Wednesday
-
-
- -
-
- Thursday
-
-
- -
-
- Friday
-
-
- -
-
- Saturday
-
-
- -
-
- Sunday
-
-
-
-
- -
-
-
- at
-
-
-
- -
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
-
-
-
- Every
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.
-
-
- days
-
-
- 1
-
-
- 0.000000000000000
-
-
- 365.100000000000023
-
-
- 1.000000000000000
-
-
- 1.000000000000000
-
-
-
-
-
- -
-
-
-
+
+
+ Qt::Vertical
-
- true
+
+
+ 20
+ 40
+
-
+
-
@@ -343,6 +296,19 @@
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
@@ -485,54 +451,6 @@
-
- daily_button
- toggled(bool)
- day
- setEnabled(bool)
-
-
- 458
- 155
-
-
- 573
- 158
-
-
-
-
- daily_button
- toggled(bool)
- time
- setEnabled(bool)
-
-
- 458
- 155
-
-
- 684
- 157
-
-
-
-
- interval_button
- toggled(bool)
- interval
- setEnabled(bool)
-
-
- 458
- 212
-
-
- 752
- 215
-
-
-
add_title_tag
toggled(bool)
diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py
index cd5a220dc3..aa0051ca37 100644
--- a/src/calibre/web/feeds/recipes/collection.py
+++ b/src/calibre/web/feeds/recipes/collection.py
@@ -231,6 +231,7 @@ class SchedulerConfig(object):
if x.get('id', False) == recipe_id:
typ, sch, last_downloaded = self.un_serialize_schedule(x)
if typ == 'interval':
+ # Prevent downloads more frequent than once an hour
actual_interval = now - last_downloaded
nominal_interval = timedelta(days=sch)
if abs(actual_interval - nominal_interval) < \
@@ -264,11 +265,16 @@ class SchedulerConfig(object):
def serialize_schedule(self, typ, schedule):
s = E.schedule({'type':typ})
if typ == 'interval':
- if schedule < 0.1:
- schedule = 1/24.
+ if schedule < 0.04:
+ schedule = 0.04
text = '%f'%schedule
elif typ == 'day/time':
text = '%d:%d:%d'%schedule
+ elif typ in ('days_of_week', 'days_of_month'):
+ dw = ','.join(map(str, map(int, schedule[0])))
+ text = '%s:%d:%d'%(dw, schedule[1], schedule[2])
+ else:
+ raise ValueError('Unknown schedule type: %r'%typ)
s.text = text
return s
@@ -280,6 +286,11 @@ class SchedulerConfig(object):
sch = float(sch)
elif typ == 'day/time':
sch = list(map(int, sch.split(':')))
+ elif typ in ('days_of_week', 'days_of_month'):
+ parts = sch.split(':')
+ days = list(map(int, [x.strip() for x in
+ parts[0].split(',')]))
+ sch = [days, int(parts[1]), int(parts[2])]
return typ, sch, parse_date(recipe.get('last_downloaded'))
def recipe_needs_to_be_downloaded(self, recipe):
@@ -287,19 +298,48 @@ class SchedulerConfig(object):
typ, sch, ld = self.un_serialize_schedule(recipe)
except:
return False
+
+ def is_time(now, hour, minute):
+ return now.hour > hour or \
+ (now.hour == hour and now.minute >= minute)
+
+ def is_weekday(day, now):
+ return day < 0 or day > 6 or \
+ day == calendar.weekday(now.year, now.month, now.day)
+
+ def was_downloaded_already_today(ld_local, now):
+ return ld_local.date() == now.date()
+
if typ == 'interval':
return utcnow() - ld > timedelta(sch)
elif typ == 'day/time':
now = nowf()
ld_local = ld.astimezone(tzlocal())
day, hour, minute = sch
+ return is_weekday(day, now) and \
+ not was_downloaded_already_today(ld_local, now) and \
+ is_time(now, hour, minute)
+ elif typ == 'days_of_week':
+ now = nowf()
+ ld_local = ld.astimezone(tzlocal())
+ days, hour, minute = sch
+ have_day = False
+ for day in days:
+ if is_weekday(day, now):
+ have_day = True
+ break
+ return have_day and \
+ not was_downloaded_already_today(ld_local, now) and \
+ is_time(now, hour, minute)
+ elif typ == 'days_of_month':
+ now = nowf()
+ ld_local = ld.astimezone(tzlocal())
+ days, hour, minute = sch
+ have_day = now.day in days
+ return have_day and \
+ not was_downloaded_already_today(ld_local, now) and \
+ is_time(now, hour, minute)
- is_today = day < 0 or day > 6 or \
- day == calendar.weekday(now.year, now.month, now.day)
- is_time = now.hour > hour or \
- (now.hour == hour and now.minute >= minute)
- was_downloaded_already_today = ld_local.date() == now.date()
- return is_today and not was_downloaded_already_today and is_time
return False
def set_account_info(self, urn, un, pw):