Implement ability to specify a time of day for scheduled news downloads. Fixes #1397 (Inability to Specify exactly download schedule for news)

This commit is contained in:
Kovid Goyal 2009-01-26 16:18:54 -08:00
parent fbaca75458
commit 0bb41a00a5
4 changed files with 253 additions and 98 deletions

View File

@ -7,11 +7,11 @@ __docformat__ = 'restructuredtext en'
Scheduler for automated recipe downloads Scheduler for automated recipe downloads
''' '''
import sys, copy import sys, copy, time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \ from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \
QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \ QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \
QFile, QObject, QTimer, QMutex, QMenu, QAction QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime
from calibre import english_sort from calibre import english_sort
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
@ -66,7 +66,10 @@ class Recipe(object):
return self.id == getattr(other, 'id', None) return self.id == getattr(other, 'id', None)
def __repr__(self): def __repr__(self):
return u'%s|%s|%s|%s'%(self.id, self.title, self.last_downloaded.ctime(), self.schedule) schedule = self.schedule
if schedule and schedule > 1e5:
schedule = decode_schedule(schedule)
return u'%s|%s|%s|%s'%(self.id, self.title, self.last_downloaded.ctime(), schedule)
builtin_recipes = [Recipe(m, r, True) for r, m in zip(recipes, recipe_modules)] builtin_recipes = [Recipe(m, r, True) for r, m in zip(recipes, recipe_modules)]
@ -169,6 +172,11 @@ class RecipeModel(QAbstractListModel, SearchQueryParser):
return QVariant(icon) return QVariant(icon)
return NONE return NONE
def update_recipe_schedule(self, recipe):
for srecipe in self.recipes:
if srecipe == recipe:
srecipe.schedule = recipe.schedule
class Search(QLineEdit): class Search(QLineEdit):
@ -210,7 +218,17 @@ class Search(QLineEdit):
text = unicode(self.text()) text = unicode(self.text())
self.emit(SIGNAL('search(PyQt_PyObject)'), text) self.emit(SIGNAL('search(PyQt_PyObject)'), text)
def encode_schedule(day, hour, minute):
day = 1e7 * (day+1)
hour = 1e4 * (hour+1)
return day + hour + minute + 1
def decode_schedule(num):
raw = '%d'%int(num)
day = int(raw[0])
hour = int(raw[2:4])
minute = int(raw[-2:])
return day-1, hour-1, minute-1
class SchedulerDialog(QDialog, Ui_Dialog): class SchedulerDialog(QDialog, Ui_Dialog):
@ -228,17 +246,22 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.connect(self.username, SIGNAL('textEdited(QString)'), self.set_account_info) self.connect(self.username, SIGNAL('textEdited(QString)'), self.set_account_info)
self.connect(self.password, SIGNAL('textEdited(QString)'), self.set_account_info) self.connect(self.password, SIGNAL('textEdited(QString)'), self.set_account_info)
self.connect(self.schedule, SIGNAL('stateChanged(int)'), self.do_schedule) self.connect(self.schedule, SIGNAL('stateChanged(int)'), self.do_schedule)
self.connect(self.schedule, SIGNAL('stateChanged(int)'),
lambda state: self.interval.setEnabled(state == Qt.Checked))
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.connect(self.show_password, SIGNAL('stateChanged(int)'),
lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password)) lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password))
self.connect(self.interval, SIGNAL('valueChanged(double)'), self.do_schedule) self.connect(self.interval, SIGNAL('valueChanged(double)'), self.do_schedule)
self.connect(self.day, SIGNAL('currentIndexChanged(int)'), self.do_schedule)
self.connect(self.time, SIGNAL('timeChanged(QTime)'), self.do_schedule)
for button in (self.daily_button, self.interval_button):
self.connect(button, SIGNAL('toggled(bool)'), self.do_schedule)
self.connect(self.search, SIGNAL('search(PyQt_PyObject)'), self._model.search) self.connect(self.search, SIGNAL('search(PyQt_PyObject)'), self._model.search)
self.connect(self._model, SIGNAL('modelReset()'), lambda : self.detail_box.setVisible(False)) self.connect(self._model, SIGNAL('modelReset()'), lambda : self.detail_box.setVisible(False))
self.connect(self.download, SIGNAL('clicked()'), self.download_now) self.connect(self.download, SIGNAL('clicked()'), self.download_now)
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
self.old_news.setValue(gconf['oldest_news']) self.old_news.setValue(gconf['oldest_news'])
self.rnumber.setText(_('%d recipes')%self._model.rowCount(None)) self.rnumber.setText(_('%d recipes')%self._model.rowCount(None))
for day in (_('day'), _('Monday'), _('Tuesday'), _('Wednesday'),
_('Thursday'), _('Friday'), _('Saturday'), _('Sunday')):
self.day.addItem(day)
def download_now(self): def download_now(self):
recipe = self._model.data(self.recipes.currentIndex(), Qt.UserRole) recipe = self._model.data(self.recipes.currentIndex(), Qt.UserRole)
@ -252,6 +275,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
config[key] = (username, password) if username and password else None config[key] = (username, password) if username and password else None
def do_schedule(self, *args): def do_schedule(self, *args):
if not getattr(self, 'allow_scheduling', False):
return
recipe = self.recipes.currentIndex() recipe = self.recipes.currentIndex()
if not recipe.isValid(): if not recipe.isValid():
return return
@ -263,17 +288,26 @@ class SchedulerDialog(QDialog, Ui_Dialog):
else: else:
recipe.last_downloaded = datetime.fromordinal(1) recipe.last_downloaded = datetime.fromordinal(1)
recipes.append(recipe) recipes.append(recipe)
recipe.schedule = self.interval.value()
if recipe.schedule < 0.1:
recipe.schedule = 1/24.
if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]: if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]:
error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_() error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_()
self.schedule.setCheckState(Qt.Unchecked) self.schedule.setCheckState(Qt.Unchecked)
return return
if self.interval_button.isChecked():
recipe.schedule = self.interval.value()
if recipe.schedule < 0.1:
recipe.schedule = 1/24.
else:
day_of_week = self.day.currentIndex() - 1
if day_of_week < 0:
day_of_week = 7
t = self.time.time()
hour, minute = t.hour(), t.minute()
recipe.schedule = encode_schedule(day_of_week, hour, minute)
else: else:
if recipe in recipes: if recipe in recipes:
recipes.remove(recipe) recipes.remove(recipe)
save_recipes(recipes) save_recipes(recipes)
self._model.update_recipe_schedule(recipe)
self.emit(SIGNAL('new_schedule(PyQt_PyObject)'), recipes) self.emit(SIGNAL('new_schedule(PyQt_PyObject)'), recipes)
def show_recipe(self, index): def show_recipe(self, index):
@ -282,8 +316,26 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.title.setText(recipe.title) self.title.setText(recipe.title)
self.author.setText(_('Created by: ') + recipe.author) self.author.setText(_('Created by: ') + recipe.author)
self.description.setText(recipe.description if recipe.description else '') self.description.setText(recipe.description if recipe.description else '')
self.allow_scheduling = False
schedule = -1 if recipe.schedule is None else recipe.schedule
if schedule < 1e5 and schedule >= 0:
self.interval.setValue(schedule)
self.interval_button.setChecked(True)
self.day.setEnabled(False), self.time.setEnabled(False)
else:
if schedule > 0:
day, hour, minute = decode_schedule(schedule)
else:
day, hour, minute = 7, 12, 0
if day == 7:
day = -1
self.day.setCurrentIndex(day+1)
self.time.setTime(QTime(hour, minute))
self.daily_button.setChecked(True)
self.interval_button.setChecked(False)
self.interval.setEnabled(False)
self.schedule.setChecked(recipe.schedule is not None) self.schedule.setChecked(recipe.schedule is not None)
self.interval.setValue(recipe.schedule if recipe.schedule is not None else 1) self.allow_scheduling = True
self.detail_box.setVisible(True) self.detail_box.setVisible(True)
self.account.setVisible(recipe.needs_subscription) self.account.setVisible(recipe.needs_subscription)
self.interval.setEnabled(self.schedule.checkState() == Qt.Checked) self.interval.setEnabled(self.schedule.checkState() == Qt.Checked)
@ -365,13 +417,22 @@ class Scheduler(QObject):
self.dirtied = False self.dirtied = False
needs_downloading = set([]) needs_downloading = set([])
self.debug('Checking...') self.debug('Checking...')
now = datetime.utcnow() nowt = datetime.utcnow()
for recipe in self.recipes: for recipe in self.recipes:
if recipe.schedule is None: if recipe.schedule is None:
continue continue
delta = now - recipe.last_downloaded delta = nowt - recipe.last_downloaded
if delta > timedelta(days=recipe.schedule): if recipe.schedule < 1e5:
needs_downloading.add(recipe) if delta > timedelta(days=recipe.schedule):
needs_downloading.add(recipe)
else:
day, hour, minute = decode_schedule(recipe.schedule)
now = time.localtime()
day_matches = day > 6 or day == now.tm_wday
tnow = now.tm_hour*60 + now.tm_min
matches = day_matches and (hour*60+minute) < tnow
if matches and delta >= timedelta(days=1):
needs_downloading.add(recipe)
self.debug('Needs downloading:', needs_downloading) self.debug('Needs downloading:', needs_downloading)

View File

@ -6,7 +6,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>726</width> <width>726</width>
<height>551</height> <height>575</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle" >
@ -42,25 +42,12 @@
</item> </item>
<item row="0" column="1" > <item row="0" column="1" >
<layout class="QVBoxLayout" name="verticalLayout_3" > <layout class="QVBoxLayout" name="verticalLayout_3" >
<item>
<spacer name="horizontalSpacer_3" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>40</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QGroupBox" name="detail_box" > <widget class="QGroupBox" name="detail_box" >
<property name="title" > <property name="title" >
<string>Schedule for download</string> <string>Schedule for download</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2" > <layout class="QVBoxLayout" name="verticalLayout_4" >
<item> <item>
<widget class="QLabel" name="title" > <widget class="QLabel" name="title" >
<property name="font" > <property name="font" >
@ -110,70 +97,97 @@
<item> <item>
<widget class="QCheckBox" name="schedule" > <widget class="QCheckBox" name="schedule" >
<property name="text" > <property name="text" >
<string>&amp;Schedule for download every:</string> <string>&amp;Schedule for download:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout" > <widget class="QWidget" native="1" name="widget" >
<item> <property name="enabled" >
<spacer name="horizontalSpacer" > <bool>false</bool>
<property name="orientation" > </property>
<enum>Qt::Horizontal</enum> <layout class="QVBoxLayout" name="verticalLayout_2" >
</property> <item>
<property name="sizeHint" stdset="0" > <layout class="QHBoxLayout" name="horizontalLayout_2" >
<size> <item>
<width>40</width> <widget class="QRadioButton" name="daily_button" >
<height>20</height> <property name="text" >
</size> <string>Every </string>
</property> </property>
</spacer> </widget>
</item> </item>
<item> <item>
<widget class="QDoubleSpinBox" name="interval" > <widget class="QComboBox" name="day" />
<property name="sizePolicy" > </item>
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" > <item>
<horstretch>0</horstretch> <widget class="QLabel" name="label_4" >
<verstretch>0</verstretch> <property name="text" >
</sizepolicy> <string>at</string>
</property> </property>
<property name="toolTip" > </widget>
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string> </item>
</property> <item>
<property name="suffix" > <widget class="QTimeEdit" name="time" />
<string> days</string> </item>
</property> <item>
<property name="decimals" > <spacer name="horizontalSpacer" >
<number>1</number> <property name="orientation" >
</property> <enum>Qt::Horizontal</enum>
<property name="minimum" > </property>
<double>0.000000000000000</double> <property name="sizeHint" stdset="0" >
</property> <size>
<property name="maximum" > <width>40</width>
<double>365.100000000000023</double> <height>20</height>
</property> </size>
<property name="singleStep" > </property>
<double>1.000000000000000</double> </spacer>
</property> </item>
<property name="value" > </layout>
<double>1.000000000000000</double> </item>
</property> <item>
</widget> <layout class="QHBoxLayout" name="horizontalLayout" >
</item> <item>
<item> <widget class="QRadioButton" name="interval_button" >
<spacer name="horizontalSpacer_2" > <property name="text" >
<property name="orientation" > <string>Every </string>
<enum>Qt::Horizontal</enum> </property>
</property> </widget>
<property name="sizeHint" stdset="0" > </item>
<size> <item>
<width>40</width> <widget class="QDoubleSpinBox" name="interval" >
<height>20</height> <property name="sizePolicy" >
</size> <sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
</property> <horstretch>0</horstretch>
</spacer> <verstretch>0</verstretch>
</item> </sizepolicy>
</layout> </property>
<property name="toolTip" >
<string>Interval at which to download this recipe. A value of zero means that the recipe will be downloaded every hour.</string>
</property>
<property name="suffix" >
<string> days</string>
</property>
<property name="decimals" >
<number>1</number>
</property>
<property name="minimum" >
<double>0.000000000000000</double>
</property>
<property name="maximum" >
<double>365.100000000000023</double>
</property>
<property name="singleStep" >
<double>1.000000000000000</double>
</property>
<property name="value" >
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="last_downloaded" > <widget class="QLabel" name="last_downloaded" >
@ -315,8 +329,8 @@
<slot>accept()</slot> <slot>accept()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>248</x> <x>613</x>
<y>254</y> <y>824</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>157</x> <x>157</x>
@ -331,8 +345,8 @@
<slot>reject()</slot> <slot>reject()</slot>
<hints> <hints>
<hint type="sourcelabel" > <hint type="sourcelabel" >
<x>316</x> <x>681</x>
<y>260</y> <y>824</y>
</hint> </hint>
<hint type="destinationlabel" > <hint type="destinationlabel" >
<x>286</x> <x>286</x>
@ -340,5 +354,85 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>schedule</sender>
<signal>toggled(bool)</signal>
<receiver>widget</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>454</x>
<y>147</y>
</hint>
<hint type="destinationlabel" >
<x>461</x>
<y>168</y>
</hint>
</hints>
</connection>
<connection>
<sender>schedule</sender>
<signal>toggled(bool)</signal>
<receiver>widget</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>458</x>
<y>137</y>
</hint>
<hint type="destinationlabel" >
<x>461</x>
<y>169</y>
</hint>
</hints>
</connection>
<connection>
<sender>daily_button</sender>
<signal>toggled(bool)</signal>
<receiver>day</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>421</x>
<y>186</y>
</hint>
<hint type="destinationlabel" >
<x>500</x>
<y>184</y>
</hint>
</hints>
</connection>
<connection>
<sender>daily_button</sender>
<signal>toggled(bool)</signal>
<receiver>time</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>442</x>
<y>193</y>
</hint>
<hint type="destinationlabel" >
<x>603</x>
<y>183</y>
</hint>
</hints>
</connection>
<connection>
<sender>interval_button</sender>
<signal>toggled(bool)</signal>
<receiver>interval</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel" >
<x>428</x>
<y>213</y>
</hint>
<hint type="destinationlabel" >
<x>495</x>
<y>218</y>
</hint>
</hints>
</connection>
</connections> </connections>
</ui> </ui>

View File

@ -5,8 +5,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>719</width> <width>738</width>
<height>612</height> <height>640</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle" >
@ -33,8 +33,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>711</width> <width>730</width>
<height>572</height> <height>600</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3" > <layout class="QVBoxLayout" name="verticalLayout_3" >

View File

@ -46,7 +46,7 @@ What formats does |app| support conversion to/from?
| | | | | | | | | | | |
| | PDF | ✔ | ✔ | ✔ | | | PDF | ✔ | ✔ | ✔ |
| | | | | | | | | | | |
| | LRS | | ✔ | | | | LRS | | ✔ | |
+-------------------+--------+------------------+-----------------------+-----------------------+ +-------------------+--------+------------------+-----------------------+-----------------------+