Sync to trunk.

This commit is contained in:
John Schember 2011-03-06 08:00:25 -05:00
commit 392be10fa9
19 changed files with 644 additions and 316 deletions

View File

@ -97,7 +97,7 @@
author: rylsfan author: rylsfan
- title: "Historia and Buctaras" - title: "Historia and Buctaras"
author: Silviu Coatara author: Silviu Cotoara
- title: "Buffalo News" - title: "Buffalo News"
author: ChappyOnIce author: ChappyOnIce
@ -174,7 +174,7 @@
author: Ricardo Jurado author: Ricardo Jurado
- title: "Various Romanian news sources" - title: "Various Romanian news sources"
author: Silviu Coatara author: Silviu Cotoara
- title: "Osnews.pl and SwiatCzytnikow" - title: "Osnews.pl and SwiatCzytnikow"
author: Tomasz Dlugosz author: Tomasz Dlugosz

View File

@ -349,3 +349,9 @@ public_smtp_relay_delay = 301
# after a restart of calibre. # after a restart of calibre.
draw_hidden_section_indicators = True draw_hidden_section_indicators = True
#: The maximum width and height for covers saved in the calibre library
# All covers in the calibre library will be resized, preserving aspect ratio,
# to fit within this size. This is to prevent slowdowns caused by extremely
# large covers
maximum_cover_size = (1200, 1600)

View File

@ -2512,7 +2512,12 @@ class ITUNES(DriverBase):
# Refresh epub metadata # Refresh epub metadata
with open(fpath,'r+b') as zfo: with open(fpath,'r+b') as zfo:
# Touch the OPF timestamp # Touch the OPF timestamp
zf_opf = ZipFile(fpath,'r') try:
zf_opf = ZipFile(fpath,'r')
except:
raise UserFeedback("'%s' is not a valid EPUB" % metadata.title,
None,
level=UserFeedback.WARN)
fnames = zf_opf.namelist() fnames = zf_opf.namelist()
opf = [x for x in fnames if '.opf' in x][0] opf = [x for x in fnames if '.opf' in x][0]
if opf: if opf:

View File

@ -887,7 +887,7 @@ vector<char>* Reflow::render_first_page(bool use_crop_box, double x_res,
} }
pg_w *= x_res/72.; pg_w *= x_res/72.;
pg_h *= x_res/72.; pg_h *= y_res/72.;
int x=0, y=0; int x=0, y=0;
this->doc->displayPageSlice(out, pg, x_res, y_res, 0, this->doc->displayPageSlice(out, pg, x_res, y_res, 0,

View File

@ -8,9 +8,11 @@ Scheduler for automated recipe downloads
''' '''
from datetime import timedelta from datetime import timedelta
import calendar, textwrap
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \ from PyQt4.Qt import QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout, \
QAction, QIcon, QMutex, QTimer, pyqtSignal QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout, \
QCheckBox, QTimeEdit, QLabel, QLineEdit, QDoubleSpinBox
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
from calibre.gui2 import config as gconf, error_dialog from calibre.gui2 import config as gconf, error_dialog
@ -18,9 +20,173 @@ from calibre.web.feeds.recipes.model import RecipeModel
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import utcnow from calibre.utils.date import utcnow
from calibre.utils.network import internet_connected from calibre.utils.network import internet_connected
from calibre.utils.ordered_dict import OrderedDict
from calibre import force_unicode
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(force_unicode(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): 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): def __init__(self, recipe_model, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
@ -30,6 +196,11 @@ class SchedulerDialog(QDialog, Ui_Dialog):
_('%s news sources') % _('%s news sources') %
self.recipe_model.showing_count) 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.initialize('scheduler_search_history')
self.search.setMinimumContentsLength(15) self.search.setMinimumContentsLength(15)
self.search.search.connect(self.recipe_model.search) self.search.search.connect(self.recipe_model.search)
@ -43,29 +214,43 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.detail_box.setVisible(False) self.detail_box.setVisible(False)
self.download_button.setVisible(False) self.download_button.setVisible(False)
self.recipes.currentChanged = self.current_changed 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.schedule.stateChanged[int].connect(self.toggle_schedule_info)
self.toggle_schedule_info) self.show_password.stateChanged[int].connect(self.set_pw_echo_mode)
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.download_button.clicked.connect(self.download_clicked)
lambda state: self.password.setEchoMode(self.password.Normal if state == Qt.Checked else self.password.Password)) self.download_all_button.clicked.connect(
self.connect(self.download_button, SIGNAL('clicked()'),
self.download_clicked)
self.connect(self.download_all_button, SIGNAL('clicked()'),
self.download_all_clicked) self.download_all_clicked)
self.old_news.setValue(gconf['oldest_news']) 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): def keyPressEvent(self, ev):
if ev.key() not in (Qt.Key_Enter, Qt.Key_Return): if ev.key() not in (Qt.Key_Enter, Qt.Key_Return):
return QDialog.keyPressEvent(self, ev) return QDialog.keyPressEvent(self, ev)
def break_cycles(self): def break_cycles(self):
self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'), try:
self.search_done) self.recipe_model.searched.disconnect(self.search_done)
self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'), self.recipe_model.searched.disconnect(self.search.search_done)
self.search.search_done) self.search.search.disconnect()
self.search.search.disconnect() self.download.disconnect()
except:
pass
self.recipe_model = None self.recipe_model = None
def search_done(self, *args): def search_done(self, *args):
@ -74,8 +259,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
def toggle_schedule_info(self, *args): def toggle_schedule_info(self, *args):
enabled = self.schedule.isChecked() 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) getattr(self, x).setEnabled(enabled)
self.schedule_stack.setEnabled(enabled)
self.last_downloaded.setVisible(enabled) self.last_downloaded.setVisible(enabled)
def current_changed(self, current, previous): def current_changed(self, current, previous):
@ -97,14 +283,14 @@ class SchedulerDialog(QDialog, Ui_Dialog):
return False return False
return QDialog.accept(self) return QDialog.accept(self)
def download_clicked(self): def download_clicked(self, *args):
self.commit() self.commit()
if self.commit() and self.current_urn: 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(): if self.commit() and self.commit():
self.emit(SIGNAL('download(PyQt_PyObject)'), None) self.download.emit(None)
@property @property
def current_urn(self): def current_urn(self):
@ -130,16 +316,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.set_account_info(urn, un, pw) self.recipe_model.set_account_info(urn, un, pw)
if self.schedule.isChecked(): if self.schedule.isChecked():
schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time' schedule_type, schedule = \
if schedule_type == 'interval': self.schedule_stack.currentWidget().schedule
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)
self.recipe_model.schedule_recipe(urn, schedule_type, schedule) self.recipe_model.schedule_recipe(urn, schedule_type, schedule)
else: else:
self.recipe_model.un_schedule_recipe(urn) self.recipe_model.un_schedule_recipe(urn)
@ -192,27 +370,27 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.schedule.setChecked(scheduled) self.schedule.setChecked(scheduled)
self.toggle_schedule_info() self.toggle_schedule_info()
self.last_downloaded.setText(_('Last downloaded: never')) self.last_downloaded.setText(_('Last downloaded: never'))
ld_text = _('never')
if scheduled: if scheduled:
typ, sch, last_downloaded = schedule_info 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 d = utcnow() - last_downloaded
def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60 def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60
hours, minutes = hm(d.seconds) hours, minutes = hm(d.seconds)
tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes) tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes)
if d < timedelta(days=366): 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 add_title_tag, custom_tags, keep_issues = customize_info
self.add_title_tag.setChecked(add_title_tag) self.add_title_tag.setChecked(add_title_tag)
self.custom_tags.setText(u', '.join(custom_tags)) self.custom_tags.setText(u', '.join(custom_tags))
self.last_downloaded.setText(_('Last downloaded:') + ' ' + ld_text)
try: try:
keep_issues = int(keep_issues) keep_issues = int(keep_issues)
except: except:
@ -233,7 +411,8 @@ class Scheduler(QObject):
QObject.__init__(self, parent) QObject.__init__(self, parent)
self.internet_connection_failed = False self.internet_connection_failed = False
self._parent = parent self._parent = parent
self.recipe_model = RecipeModel(db) self.recipe_model = RecipeModel()
self.db = db
self.lock = QMutex(QMutex.Recursive) self.lock = QMutex(QMutex.Recursive)
self.download_queue = set([]) self.download_queue = set([])
@ -241,9 +420,9 @@ class Scheduler(QObject):
self.news_icon = QIcon(I('news.png')) self.news_icon = QIcon(I('news.png'))
self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self) self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self)
self.news_menu.addAction(self.scheduler_action) 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.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.addAction(self.cac)
self.news_menu.addSeparator() self.news_menu.addSeparator()
self.all_action = self.news_menu.addAction( self.all_action = self.news_menu.addAction(
@ -252,10 +431,12 @@ class Scheduler(QObject):
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.start(int(self.INTERVAL * 60 * 1000)) 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'] self.oldest = gconf['oldest_news']
QTimer.singleShot(5 * 1000, self.oldest_check) QTimer.singleShot(5 * 1000, self.oldest_check)
self.database_changed = self.recipe_model.database_changed
def database_changed(self, db):
self.db = db
def oldest_check(self): def oldest_check(self):
if self.oldest > 0: if self.oldest > 0:
@ -276,8 +457,7 @@ class Scheduler(QObject):
self.lock.lock() self.lock.lock()
try: try:
d = SchedulerDialog(self.recipe_model) d = SchedulerDialog(self.recipe_model)
self.connect(d, SIGNAL('download(PyQt_PyObject)'), d.download.connect(self.download_clicked)
self.download_clicked)
d.exec_() d.exec_()
gconf['oldest_news'] = self.oldest = d.old_news.value() gconf['oldest_news'] = self.oldest = d.old_news.value()
d.break_cycles() d.break_cycles()
@ -372,7 +552,6 @@ class Scheduler(QObject):
if __name__ == '__main__': if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt() is_ok_to_use_qt()
from calibre.library.database2 import LibraryDatabase2 d = SchedulerDialog(RecipeModel())
d = SchedulerDialog(RecipeModel(LibraryDatabase2('/home/kovid/documents/library')))
d.exec_() d.exec_()

View File

@ -61,7 +61,7 @@
<attribute name="title"> <attribute name="title">
<string>&amp;Schedule</string> <string>&amp;Schedule</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="blurb"> <widget class="QLabel" name="blurb">
<property name="text"> <property name="text">
@ -76,141 +76,94 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="schedule"> <spacer name="verticalSpacer_3">
<property name="text"> <property name="orientation">
<string>&amp;Schedule for download:</string> <enum>Qt::Vertical</enum>
</property> </property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="schedule">
<property name="text">
<string>&amp;Schedule for download:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="days_of_week">
<property name="text">
<string>Days of week</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="days_of_month">
<property name="text">
<string>Days of month</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="every_x_days">
<property name="text">
<string>Every x days</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="schedule_stack">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>75</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="last_downloaded">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_2"> <spacer name="verticalSpacer_4">
<item> <property name="orientation">
<widget class="QRadioButton" name="daily_button"> <enum>Qt::Vertical</enum>
<property name="text">
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="day">
<item>
<property name="text">
<string>day</string>
</property>
</item>
<item>
<property name="text">
<string>Monday</string>
</property>
</item>
<item>
<property name="text">
<string>Tuesday</string>
</property>
</item>
<item>
<property name="text">
<string>Wednesday</string>
</property>
</item>
<item>
<property name="text">
<string>Thursday</string>
</property>
</item>
<item>
<property name="text">
<string>Friday</string>
</property>
</item>
<item>
<property name="text">
<string>Saturday</string>
</property>
</item>
<item>
<property name="text">
<string>Sunday</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>at</string>
</property>
</widget>
</item>
<item>
<widget class="QTimeEdit" name="time"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="interval_button">
<property name="text">
<string>Every </string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="interval">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</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>
<item>
<widget class="QLabel" name="last_downloaded">
<property name="text">
<string/>
</property> </property>
<property name="wordWrap"> <property name="sizeHint" stdset="0">
<bool>true</bool> <size>
<width>20</width>
<height>40</height>
</size>
</property> </property>
</widget> </spacer>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="account"> <widget class="QGroupBox" name="account">
@ -343,6 +296,19 @@
</widget> </widget>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QPushButton" name="download_button"> <widget class="QPushButton" name="download_button">
<property name="text"> <property name="text">
@ -485,54 +451,6 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>daily_button</sender>
<signal>toggled(bool)</signal>
<receiver>day</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>458</x>
<y>155</y>
</hint>
<hint type="destinationlabel">
<x>573</x>
<y>158</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>458</x>
<y>155</y>
</hint>
<hint type="destinationlabel">
<x>684</x>
<y>157</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>458</x>
<y>212</y>
</hint>
<hint type="destinationlabel">
<x>752</x>
<y>215</y>
</hint>
</hints>
</connection>
<connection> <connection>
<sender>add_title_tag</sender> <sender>add_title_tag</sender>
<signal>toggled(bool)</signal> <signal>toggled(bool)</signal>

View File

@ -6,11 +6,11 @@ import time, os
from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \ from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \
QVariant QVariant
from calibre.web.feeds.recipes import compile_recipe from calibre.web.feeds.recipes import compile_recipe, custom_recipes
from calibre.web.feeds.news import AutomaticNewsRecipe from calibre.web.feeds.news import AutomaticNewsRecipe
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
from calibre.gui2 import error_dialog, question_dialog, open_url, \ from calibre.gui2 import error_dialog, question_dialog, open_url, \
choose_files, ResizableDialog, NONE choose_files, ResizableDialog, NONE, open_local_file
from calibre.gui2.widgets import PythonHighlighter from calibre.gui2.widgets import PythonHighlighter
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -93,6 +93,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
self.connect(self.load_button, SIGNAL('clicked()'), self.load) self.connect(self.load_button, SIGNAL('clicked()'), self.load)
self.connect(self.builtin_recipe_button, SIGNAL('clicked()'), self.add_builtin_recipe) self.connect(self.builtin_recipe_button, SIGNAL('clicked()'), self.add_builtin_recipe)
self.connect(self.share_button, SIGNAL('clicked()'), self.share) self.connect(self.share_button, SIGNAL('clicked()'), self.share)
self.show_recipe_files_button.clicked.connect(self.show_recipe_files)
self.connect(self.down_button, SIGNAL('clicked()'), self.down) self.connect(self.down_button, SIGNAL('clicked()'), self.down)
self.connect(self.up_button, SIGNAL('clicked()'), self.up) self.connect(self.up_button, SIGNAL('clicked()'), self.up)
self.connect(self.add_profile_button, SIGNAL('clicked(bool)'), self.connect(self.add_profile_button, SIGNAL('clicked(bool)'),
@ -102,6 +103,10 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
self.connect(self.toggle_mode_button, SIGNAL('clicked(bool)'), self.toggle_mode) self.connect(self.toggle_mode_button, SIGNAL('clicked(bool)'), self.toggle_mode)
self.clear() self.clear()
def show_recipe_files(self, *args):
bdir = os.path.dirname(custom_recipes.file_path)
open_local_file(bdir)
def break_cycles(self): def break_cycles(self):
self.recipe_model = self._model.recipe_model = None self.recipe_model = self._model.recipe_model = None
self.available_profiles = None self.available_profiles = None
@ -366,8 +371,7 @@ class %(classname)s(%(base_class)s):
if __name__ == '__main__': if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt() is_ok_to_use_qt()
from calibre.library.database2 import LibraryDatabase2
from calibre.web.feeds.recipes.model import RecipeModel from calibre.web.feeds.recipes.model import RecipeModel
d=UserProfiles(None, RecipeModel(LibraryDatabase2('/home/kovid/documents/library'))) d=UserProfiles(None, RecipeModel())
d.exec_() d.exec_()

View File

@ -35,7 +35,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>730</width> <width>730</width>
<height>600</height> <height>601</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
@ -102,6 +102,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="show_recipe_files_button">
<property name="text">
<string>S&amp;how recipe files</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="builtin_recipe_button"> <widget class="QPushButton" name="builtin_recipe_button">
<property name="text"> <property name="text">

View File

@ -687,14 +687,14 @@ class BooksModel(QAbstractTableModel): # {{{
idx = self.custom_columns[col]['rec_index'] idx = self.custom_columns[col]['rec_index']
datatype = self.custom_columns[col]['datatype'] datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments', 'composite', 'enumeration'): if datatype in ('text', 'comments', 'composite', 'enumeration'):
self.dc[col] = functools.partial(text_type, idx=idx, mult=self.custom_columns[col]['is_multiple']
mult=self.custom_columns[col]['is_multiple']) self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
if datatype == 'composite': if datatype in ['text', 'composite', 'enumeration'] and not mult:
csort = self.custom_columns[col]['display'].get('composite_sort', 'text') if self.custom_columns[col]['display'].get('use_decorations', False):
if csort == 'bool':
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] != 'no') bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no')
elif datatype in ('int', 'float'): elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx) self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime': elif datatype == 'datetime':
@ -703,7 +703,8 @@ class BooksModel(QAbstractTableModel): # {{{
self.dc[col] = functools.partial(bool_type, idx=idx) self.dc[col] = functools.partial(bool_type, idx=idx)
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] != 'no') bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no')
elif datatype == 'rating': elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx) self.dc[col] = functools.partial(rating_type, idx=idx)
elif datatype == 'series': elif datatype == 'series':

View File

@ -121,6 +121,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
elif ct == 'enumeration': elif ct == 'enumeration':
self.enum_box.setText(','.join(c['display'].get('enum_values', []))) self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
self.datatype_changed() self.datatype_changed()
if ct in ['text', 'composite', 'enumeration']:
self.use_decorations.setChecked(c['display'].get('use_decorations', False))
self.exec_() self.exec_()
def shortcut_activated(self, url): def shortcut_activated(self, url):
@ -161,6 +163,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
getattr(self, 'composite_'+x).setVisible(col_type == 'composite') getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
for x in ('box', 'default_label', 'label'): for x in ('box', 'default_label', 'label'):
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration'])
def accept(self): def accept(self):
col = unicode(self.column_name_box.text()).strip() col = unicode(self.column_name_box.text()).strip()
@ -233,6 +236,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'list more than once').format(l[i])) 'list more than once').format(l[i]))
display_dict = {'enum_values': l} display_dict = {'enum_values': l}
if col_type in ['text', 'composite', 'enumeration']:
display_dict['use_decorations'] = self.use_decorations.checkState()
if not self.editing_col: if not self.editing_col:
db.field_metadata db.field_metadata
self.parent.custcols[key] = { self.parent.custcols[key] = {

View File

@ -88,23 +88,58 @@
</widget> </widget>
</item> </item>
<item row="2" column="2"> <item row="2" column="2">
<widget class="QComboBox" name="column_type_box"> <layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="sizePolicy"> <item>
<sizepolicy hsizetype="Maximum" vsizetype="Fixed"> <widget class="QComboBox" name="column_type_box">
<horstretch>0</horstretch> <property name="sizePolicy">
<verstretch>0</verstretch> <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
</sizepolicy> <horstretch>0</horstretch>
</property> <verstretch>0</verstretch>
<property name="minimumSize"> </sizepolicy>
<size> </property>
<width>70</width> <property name="minimumSize">
<height>0</height> <size>
</size> <width>70</width>
</property> <height>0</height>
<property name="toolTip"> </size>
<string>What kind of information will be kept in the column.</string> </property>
</property> <property name="toolTip">
</widget> <string>What kind of information will be kept in the column.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="use_decorations">
<property name="text">
<string>Show checkmarks</string>
</property>
<property name="toolTip">
<string>Show check marks in the GUI. Values of 'yes', 'checked', and 'true'
will show a green check. Values of 'no', 'unchecked', and 'false' will show a red X.
Everything else will show nothing.</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_27">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item> </item>
<item row="4" column="2"> <item row="4" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_3"> <layout class="QHBoxLayout" name="horizontalLayout_3">

View File

@ -5103,6 +5103,19 @@ Author '{0}':
recommendations.append(('book_producer',opts.output_profile, recommendations.append(('book_producer',opts.output_profile,
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
# If cover exists, use it
try:
search_text = 'title:"%s" author:%s' % (
opts.catalog_title.replace('"', '\\"'), 'calibre')
matches = db.search(search_text, return_matches=True)
if matches:
cpath = db.cover(matches[0], index_is_id=True, as_path=True)
if cpath and os.path.exists(cpath):
recommendations.append(('cover', cpath,
OptionRecommendation.HIGH))
except:
pass
# Run ebook-convert # Run ebook-convert
from calibre.ebooks.conversion.plumber import Plumber from calibre.ebooks.conversion.plumber import Plumber
plumber = Plumber(os.path.join(catalog.catalogPath, plumber = Plumber(os.path.join(catalog.catalogPath,

View File

@ -1187,12 +1187,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.clean_custom() self.clean_custom()
self.conn.commit() self.conn.commit()
def get_recipes(self):
return self.conn.get('SELECT id, script FROM feeds')
def get_recipe(self, id):
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
def get_books_for_category(self, category, id_): def get_books_for_category(self, category, id_):
ans = set([]) ans = set([])
@ -3113,8 +3107,4 @@ books_series_link feeds
s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,)) s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,))
return [x[0] for x in s] return [x[0] for x in s]
def get_custom_recipes(self):
for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'):
yield id, title, script

View File

@ -582,4 +582,22 @@ class SchemaUpgrade(object):
# statements # statements
self.conn.executescript(script) self.conn.executescript(script)
def upgrade_version_19(self):
recipes = self.conn.get('SELECT id,title,script FROM feeds')
if recipes:
from calibre.web.feeds.recipes import custom_recipes, \
custom_recipe_filename
bdir = os.path.dirname(custom_recipes.file_path)
for id_, title, script in recipes:
existing = frozenset(map(int, custom_recipes.iterkeys()))
if id_ in existing:
id_ = max(existing) + 1000
id_ = str(id_)
fname = custom_recipe_filename(id_, title)
custom_recipes[id_] = (title, fname)
if isinstance(script, unicode):
script = script.encode('utf-8')
with open(os.path.join(bdir, fname), 'wb') as f:
f.write(script)

View File

@ -666,15 +666,13 @@ class BrowseServer(object):
if add_category_links: if add_category_links:
added_key = False added_key = False
fm = mi.metadata_for_field(key) fm = mi.metadata_for_field(key)
if val and fm and fm['is_category'] and \ if val and fm and fm['is_category'] and not fm['is_csp'] and\
key != 'formats' and fm['datatype'] not in ['rating']: key != 'formats' and fm['datatype'] not in ['rating']:
categories = mi.get(key) categories = mi.get(key)
if isinstance(categories, basestring): if isinstance(categories, basestring):
categories = [categories] categories = [categories]
dbtags = [] dbtags = []
for category in categories: for category in categories:
if category not in ccache:
continue
dbtag = None dbtag = None
for tag in ccache[key]: for tag in ccache[key]:
if tag.name == category: if tag.name == category:

View File

@ -12,6 +12,34 @@ from calibre.constants import __appname__, __version__
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre import fit_image from calibre import fit_image
def _data_to_image(data):
if isinstance(data, Image):
img = data
else:
img = Image()
img.load(data)
return img
def minify_image(data, minify_to=(1200, 1600), preserve_aspect_ratio=True):
'''
Minify image to specified size if image is bigger than specified
size and return minified image, otherwise, original image is
returned.
:param data: Image data as bytestring or Image object
:param minify_to: A tuple (width, height) to specify target size
:param preserve_aspect_ratio: whether preserve original aspect ratio
'''
img = _data_to_image(data)
owidth, oheight = img.size
nwidth, nheight = minify_to
if owidth <= nwidth and oheight <= nheight:
return img
if preserve_aspect_ratio:
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
img.size = (nwidth, nheight)
return img
def normalize_format_name(fmt): def normalize_format_name(fmt):
fmt = fmt.lower() fmt = fmt.lower()
if fmt == 'jpeg': if fmt == 'jpeg':
@ -19,7 +47,7 @@ def normalize_format_name(fmt):
return fmt return fmt
def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
return_data=False, compression_quality=90): return_data=False, compression_quality=90, minify_to=None):
''' '''
Saves image in data to path, in the format specified by the path Saves image in data to path, in the format specified by the path
extension. Removes any transparency. If there is no transparency and no extension. Removes any transparency. If there is no transparency and no
@ -32,14 +60,13 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
compression (lossless). compression (lossless).
:param bgcolor: The color for transparent pixels. Must be specified in hex. :param bgcolor: The color for transparent pixels. Must be specified in hex.
:param resize_to: A tuple (width, height) or None for no resizing :param resize_to: A tuple (width, height) or None for no resizing
:param minify_to: A tuple (width, height) to specify target size. The image
will be resized to fit into this target size. If None the value from the
tweak is used.
''' '''
changed = False changed = False
if isinstance(data, Image): img = _data_to_image(data)
img = data
else:
img = Image()
img.load(data)
orig_fmt = normalize_format_name(img.format) orig_fmt = normalize_format_name(img.format)
fmt = os.path.splitext(path)[1] fmt = os.path.splitext(path)[1]
fmt = normalize_format_name(fmt[1:]) fmt = normalize_format_name(fmt[1:])
@ -47,11 +74,18 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None,
if resize_to is not None: if resize_to is not None:
img.size = (resize_to[0], resize_to[1]) img.size = (resize_to[0], resize_to[1])
changed = True changed = True
owidth, oheight = img.size
nwidth, nheight = tweaks['maximum_cover_size'] if minify_to is None else minify_to
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
if scaled:
img.size = (nwidth, nheight)
changed = True
if img.has_transparent_pixels(): if img.has_transparent_pixels():
canvas = create_canvas(img.size[0], img.size[1], bgcolor) canvas = create_canvas(img.size[0], img.size[1], bgcolor)
canvas.compose(img) canvas.compose(img)
img = canvas img = canvas
changed = True changed = True
if not changed: if not changed:
changed = fmt != orig_fmt changed = fmt != orig_fmt

View File

@ -10,6 +10,7 @@ from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, \
from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre import __appname__, english_sort from calibre import __appname__, english_sort
from calibre.utils.config import JSONConfig
BeautifulSoup, time, english_sort BeautifulSoup, time, english_sort
@ -17,6 +18,14 @@ basic_recipes = (BasicNewsRecipe, AutomaticNewsRecipe, CustomIndexRecipe,
CalibrePeriodical) CalibrePeriodical)
_tdir = None _tdir = None
_crep = 0 _crep = 0
custom_recipes = JSONConfig('custom_recipes/index.json')
def custom_recipe_filename(id_, title):
from calibre.utils.filenames import ascii_filename
return ascii_filename(title[:50]) + \
('_%s.recipe'%id_)
def compile_recipe(src): def compile_recipe(src):
''' '''
Compile the code in src and return the first object that is a recipe or profile. Compile the code in src and return the first object that is a recipe or profile.

View File

@ -88,19 +88,89 @@ def serialize_builtin_recipes():
def get_builtin_recipe_collection(): def get_builtin_recipe_collection():
return etree.parse(P('builtin_recipes.xml')).getroot() return etree.parse(P('builtin_recipes.xml')).getroot()
def get_custom_recipe_collection(db): def get_custom_recipe_collection(*args):
from calibre.web.feeds.recipes import compile_recipe from calibre.web.feeds.recipes import compile_recipe, \
custom_recipes
bdir = os.path.dirname(custom_recipes.file_path)
rmap = {} rmap = {}
for id, title, recipe in db.get_custom_recipes(): for id_, x in custom_recipes.iteritems():
title, fname = x
recipe = os.path.join(bdir, fname)
try: try:
recipe = open(recipe, 'rb').read().decode('utf-8')
recipe_class = compile_recipe(recipe) recipe_class = compile_recipe(recipe)
if recipe_class is not None: if recipe_class is not None:
rmap['custom:%d'%id] = recipe_class rmap['custom:%s'%id_] = recipe_class
except: except:
import traceback
traceback.print_exc()
continue continue
return etree.fromstring(serialize_collection(rmap)) return etree.fromstring(serialize_collection(rmap))
def update_custom_recipe(id_, title, script):
from calibre.web.feeds.recipes import custom_recipes, \
custom_recipe_filename
id_ = str(int(id_))
existing = custom_recipes.get(id_, None)
bdir = os.path.dirname(custom_recipes.file_path)
if existing is None:
fname = custom_recipe_filename(id_, title)
else:
fname = existing[1]
if isinstance(script, unicode):
script = script.encode('utf-8')
custom_recipes[id_] = (title, fname)
with open(os.path.join(bdir, fname), 'wb') as f:
f.write(script)
def add_custom_recipe(title, script):
from calibre.web.feeds.recipes import custom_recipes, \
custom_recipe_filename
id_ = 1000
keys = tuple(map(int, custom_recipes.iterkeys()))
if keys:
id_ = max(keys)+1
id_ = str(id_)
bdir = os.path.dirname(custom_recipes.file_path)
fname = custom_recipe_filename(id_, title)
if isinstance(script, unicode):
script = script.encode('utf-8')
custom_recipes[id_] = (title, fname)
with open(os.path.join(bdir, fname), 'wb') as f:
f.write(script)
def remove_custom_recipe(id_):
from calibre.web.feeds.recipes import custom_recipes
id_ = str(int(id_))
existing = custom_recipes.get(id_, None)
if existing is not None:
bdir = os.path.dirname(custom_recipes.file_path)
fname = existing[1]
del custom_recipes[id_]
try:
os.remove(os.path.join(bdir, fname))
except:
pass
def get_custom_recipe(id_):
from calibre.web.feeds.recipes import custom_recipes
id_ = str(int(id_))
existing = custom_recipes.get(id_, None)
if existing is not None:
bdir = os.path.dirname(custom_recipes.file_path)
fname = existing[1]
with open(os.path.join(bdir, fname), 'rb') as f:
return f.read().decode('utf-8')
def get_builtin_recipe_titles(): def get_builtin_recipe_titles():
return [r.get('title') for r in get_builtin_recipe_collection()] return [r.get('title') for r in get_builtin_recipe_collection()]
@ -231,6 +301,7 @@ class SchedulerConfig(object):
if x.get('id', False) == recipe_id: if x.get('id', False) == recipe_id:
typ, sch, last_downloaded = self.un_serialize_schedule(x) typ, sch, last_downloaded = self.un_serialize_schedule(x)
if typ == 'interval': if typ == 'interval':
# Prevent downloads more frequent than once an hour
actual_interval = now - last_downloaded actual_interval = now - last_downloaded
nominal_interval = timedelta(days=sch) nominal_interval = timedelta(days=sch)
if abs(actual_interval - nominal_interval) < \ if abs(actual_interval - nominal_interval) < \
@ -264,11 +335,16 @@ class SchedulerConfig(object):
def serialize_schedule(self, typ, schedule): def serialize_schedule(self, typ, schedule):
s = E.schedule({'type':typ}) s = E.schedule({'type':typ})
if typ == 'interval': if typ == 'interval':
if schedule < 0.1: if schedule < 0.04:
schedule = 1/24. schedule = 0.04
text = '%f'%schedule text = '%f'%schedule
elif typ == 'day/time': elif typ == 'day/time':
text = '%d:%d:%d'%schedule 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 s.text = text
return s return s
@ -280,6 +356,11 @@ class SchedulerConfig(object):
sch = float(sch) sch = float(sch)
elif typ == 'day/time': elif typ == 'day/time':
sch = list(map(int, sch.split(':'))) 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')) return typ, sch, parse_date(recipe.get('last_downloaded'))
def recipe_needs_to_be_downloaded(self, recipe): def recipe_needs_to_be_downloaded(self, recipe):
@ -287,19 +368,48 @@ class SchedulerConfig(object):
typ, sch, ld = self.un_serialize_schedule(recipe) typ, sch, ld = self.un_serialize_schedule(recipe)
except: except:
return False 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': if typ == 'interval':
return utcnow() - ld > timedelta(sch) return utcnow() - ld > timedelta(sch)
elif typ == 'day/time': elif typ == 'day/time':
now = nowf() now = nowf()
ld_local = ld.astimezone(tzlocal()) ld_local = ld.astimezone(tzlocal())
day, hour, minute = sch 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 return False
def set_account_info(self, urn, un, pw): def set_account_info(self, urn, un, pw):

View File

@ -9,14 +9,15 @@ __docformat__ = 'restructuredtext en'
import os, copy import os, copy
from PyQt4.Qt import QAbstractItemModel, QVariant, Qt, QColor, QFont, QIcon, \ from PyQt4.Qt import QAbstractItemModel, QVariant, Qt, QColor, QFont, QIcon, \
QModelIndex, QMetaObject, pyqtSlot, pyqtSignal QModelIndex, pyqtSignal
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.gui2 import NONE from calibre.gui2 import NONE
from calibre.utils.localization import get_language from calibre.utils.localization import get_language
from calibre.web.feeds.recipes.collection import \ from calibre.web.feeds.recipes.collection import \
get_builtin_recipe_collection, get_custom_recipe_collection, \ get_builtin_recipe_collection, get_custom_recipe_collection, \
SchedulerConfig, download_builtin_recipe SchedulerConfig, download_builtin_recipe, update_custom_recipe, \
add_custom_recipe, remove_custom_recipe, get_custom_recipe
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
class NewsTreeItem(object): class NewsTreeItem(object):
@ -122,26 +123,15 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
LOCATIONS = ['all'] LOCATIONS = ['all']
searched = pyqtSignal(object) searched = pyqtSignal(object)
def __init__(self, db, *args): def __init__(self, *args):
QAbstractItemModel.__init__(self, *args) QAbstractItemModel.__init__(self, *args)
SearchQueryParser.__init__(self, locations=['all']) SearchQueryParser.__init__(self, locations=['all'])
self.db = db
self.default_icon = QVariant(QIcon(I('news.png'))) self.default_icon = QVariant(QIcon(I('news.png')))
self.custom_icon = QVariant(QIcon(I('user_profile.png'))) self.custom_icon = QVariant(QIcon(I('user_profile.png')))
self.builtin_recipe_collection = get_builtin_recipe_collection() self.builtin_recipe_collection = get_builtin_recipe_collection()
self.scheduler_config = SchedulerConfig() self.scheduler_config = SchedulerConfig()
self.do_refresh() self.do_refresh()
@pyqtSlot()
def do_database_change(self):
self.db = self.newdb
self.newdb = None
self.do_refresh()
def database_changed(self, db):
self.newdb = db
QMetaObject.invokeMethod(self, 'do_database_change', Qt.QueuedConnection)
def get_builtin_recipe(self, urn, download=True): def get_builtin_recipe(self, urn, download=True):
if download: if download:
try: try:
@ -158,23 +148,24 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
if recipe.get('id', False) == urn: if recipe.get('id', False) == urn:
if coll is self.builtin_recipe_collection: if coll is self.builtin_recipe_collection:
return self.get_builtin_recipe(urn[8:], download=download) return self.get_builtin_recipe(urn[8:], download=download)
return self.db.get_feed(int(urn[len('custom:'):])) return get_custom_recipe(int(urn[len('custom:'):]))
def update_custom_recipe(self, urn, title, script): def update_custom_recipe(self, urn, title, script):
self.db.update_feed(int(urn[len('custom:'):]), script, title) id_ = int(urn[len('custom:'):])
self.custom_recipe_collection = get_custom_recipe_collection(self.db) update_custom_recipe(id_, title, script)
self.custom_recipe_collection = get_custom_recipe_collection()
def add_custom_recipe(self, title, script): def add_custom_recipe(self, title, script):
self.db.add_feed(title, script) add_custom_recipe(title, script)
self.custom_recipe_collection = get_custom_recipe_collection(self.db) self.custom_recipe_collection = get_custom_recipe_collection()
def remove_custom_recipes(self, urns): def remove_custom_recipes(self, urns):
ids = [int(x[len('custom:'):]) for x in urns] ids = [int(x[len('custom:'):]) for x in urns]
self.db.remove_feeds(ids) for id_ in ids: remove_custom_recipe(id_)
self.custom_recipe_collection = get_custom_recipe_collection(self.db) self.custom_recipe_collection = get_custom_recipe_collection()
def do_refresh(self, restrict_to_urns=set([])): def do_refresh(self, restrict_to_urns=set([])):
self.custom_recipe_collection = get_custom_recipe_collection(self.db) self.custom_recipe_collection = get_custom_recipe_collection()
def factory(cls, parent, *args): def factory(cls, parent, *args):
args = list(args) args = list(args)