mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Sync to trunk.
This commit is contained in:
commit
392be10fa9
@ -97,7 +97,7 @@
|
||||
author: rylsfan
|
||||
|
||||
- title: "Historia and Buctaras"
|
||||
author: Silviu Coatara
|
||||
author: Silviu Cotoara
|
||||
|
||||
- title: "Buffalo News"
|
||||
author: ChappyOnIce
|
||||
@ -174,7 +174,7 @@
|
||||
author: Ricardo Jurado
|
||||
|
||||
- title: "Various Romanian news sources"
|
||||
author: Silviu Coatara
|
||||
author: Silviu Cotoara
|
||||
|
||||
- title: "Osnews.pl and SwiatCzytnikow"
|
||||
author: Tomasz Dlugosz
|
||||
|
@ -349,3 +349,9 @@ public_smtp_relay_delay = 301
|
||||
# after a restart of calibre.
|
||||
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)
|
||||
|
||||
|
@ -2512,7 +2512,12 @@ class ITUNES(DriverBase):
|
||||
# Refresh epub metadata
|
||||
with open(fpath,'r+b') as zfo:
|
||||
# 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()
|
||||
opf = [x for x in fnames if '.opf' in x][0]
|
||||
if opf:
|
||||
|
@ -887,7 +887,7 @@ vector<char>* Reflow::render_first_page(bool use_crop_box, double x_res,
|
||||
}
|
||||
|
||||
pg_w *= x_res/72.;
|
||||
pg_h *= x_res/72.;
|
||||
pg_h *= y_res/72.;
|
||||
|
||||
int x=0, y=0;
|
||||
this->doc->displayPageSlice(out, pg, x_res, y_res, 0,
|
||||
|
@ -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,173 @@ 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
|
||||
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):
|
||||
|
||||
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 +196,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 +214,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 +259,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 +283,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 +316,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 +370,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:
|
||||
@ -233,7 +411,8 @@ class Scheduler(QObject):
|
||||
QObject.__init__(self, parent)
|
||||
self.internet_connection_failed = False
|
||||
self._parent = parent
|
||||
self.recipe_model = RecipeModel(db)
|
||||
self.recipe_model = RecipeModel()
|
||||
self.db = db
|
||||
self.lock = QMutex(QMutex.Recursive)
|
||||
self.download_queue = set([])
|
||||
|
||||
@ -241,9 +420,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,10 +431,12 @@ 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
|
||||
|
||||
def database_changed(self, db):
|
||||
self.db = db
|
||||
|
||||
def oldest_check(self):
|
||||
if self.oldest > 0:
|
||||
@ -276,8 +457,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()
|
||||
@ -372,7 +552,6 @@ class Scheduler(QObject):
|
||||
if __name__ == '__main__':
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
is_ok_to_use_qt()
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
d = SchedulerDialog(RecipeModel(LibraryDatabase2('/home/kovid/documents/library')))
|
||||
d = SchedulerDialog(RecipeModel())
|
||||
d.exec_()
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
||||
<attribute name="title">
|
||||
<string>&Schedule</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="blurb">
|
||||
<property name="text">
|
||||
@ -76,141 +76,94 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="schedule">
|
||||
<property name="text">
|
||||
<string>&Schedule for download:</string>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<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>
|
||||
<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>&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>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="daily_button">
|
||||
<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/>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="account">
|
||||
@ -343,6 +296,19 @@
|
||||
</widget>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QPushButton" name="download_button">
|
||||
<property name="text">
|
||||
@ -485,54 +451,6 @@
|
||||
</hint>
|
||||
</hints>
|
||||
</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>
|
||||
<sender>add_title_tag</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
|
@ -6,11 +6,11 @@ import time, os
|
||||
from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \
|
||||
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.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
||||
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.ptempfile import PersistentTemporaryFile
|
||||
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.builtin_recipe_button, SIGNAL('clicked()'), self.add_builtin_recipe)
|
||||
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.up_button, SIGNAL('clicked()'), self.up)
|
||||
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.clear()
|
||||
|
||||
def show_recipe_files(self, *args):
|
||||
bdir = os.path.dirname(custom_recipes.file_path)
|
||||
open_local_file(bdir)
|
||||
|
||||
def break_cycles(self):
|
||||
self.recipe_model = self._model.recipe_model = None
|
||||
self.available_profiles = None
|
||||
@ -366,8 +371,7 @@ class %(classname)s(%(base_class)s):
|
||||
if __name__ == '__main__':
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
is_ok_to_use_qt()
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.web.feeds.recipes.model import RecipeModel
|
||||
d=UserProfiles(None, RecipeModel(LibraryDatabase2('/home/kovid/documents/library')))
|
||||
d=UserProfiles(None, RecipeModel())
|
||||
d.exec_()
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>730</width>
|
||||
<height>600</height>
|
||||
<height>601</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
@ -102,6 +102,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="show_recipe_files_button">
|
||||
<property name="text">
|
||||
<string>S&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>
|
||||
<widget class="QPushButton" name="builtin_recipe_button">
|
||||
<property name="text">
|
||||
|
@ -687,14 +687,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
idx = self.custom_columns[col]['rec_index']
|
||||
datatype = self.custom_columns[col]['datatype']
|
||||
if datatype in ('text', 'comments', 'composite', 'enumeration'):
|
||||
self.dc[col] = functools.partial(text_type, idx=idx,
|
||||
mult=self.custom_columns[col]['is_multiple'])
|
||||
if datatype == 'composite':
|
||||
csort = self.custom_columns[col]['display'].get('composite_sort', 'text')
|
||||
if csort == 'bool':
|
||||
mult=self.custom_columns[col]['is_multiple']
|
||||
self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
|
||||
if datatype in ['text', 'composite', 'enumeration'] and not mult:
|
||||
if self.custom_columns[col]['display'].get('use_decorations', False):
|
||||
self.dc_decorator[col] = functools.partial(
|
||||
bool_type_decorator, idx=idx,
|
||||
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] != 'no')
|
||||
bool_type_decorator, idx=idx,
|
||||
bool_cols_are_tristate=
|
||||
tweaks['bool_custom_columns_are_tristate'] != 'no')
|
||||
elif datatype in ('int', 'float'):
|
||||
self.dc[col] = functools.partial(number_type, idx=idx)
|
||||
elif datatype == 'datetime':
|
||||
@ -703,7 +703,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.dc[col] = functools.partial(bool_type, idx=idx)
|
||||
self.dc_decorator[col] = functools.partial(
|
||||
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':
|
||||
self.dc[col] = functools.partial(rating_type, idx=idx)
|
||||
elif datatype == 'series':
|
||||
|
@ -121,6 +121,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
elif ct == 'enumeration':
|
||||
self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
|
||||
self.datatype_changed()
|
||||
if ct in ['text', 'composite', 'enumeration']:
|
||||
self.use_decorations.setChecked(c['display'].get('use_decorations', False))
|
||||
self.exec_()
|
||||
|
||||
def shortcut_activated(self, url):
|
||||
@ -161,6 +163,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
|
||||
for x in ('box', 'default_label', 'label'):
|
||||
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
|
||||
self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration'])
|
||||
|
||||
def accept(self):
|
||||
col = unicode(self.column_name_box.text()).strip()
|
||||
@ -233,6 +236,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
'list more than once').format(l[i]))
|
||||
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:
|
||||
db.field_metadata
|
||||
self.parent.custcols[key] = {
|
||||
|
@ -88,23 +88,58 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QComboBox" name="column_type_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>What kind of information will be kept in the column.</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QComboBox" name="column_type_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<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 row="4" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
|
@ -5103,6 +5103,19 @@ Author '{0}':
|
||||
recommendations.append(('book_producer',opts.output_profile,
|
||||
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
|
||||
from calibre.ebooks.conversion.plumber import Plumber
|
||||
plumber = Plumber(os.path.join(catalog.catalogPath,
|
||||
|
@ -1187,12 +1187,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.clean_custom()
|
||||
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_):
|
||||
ans = set([])
|
||||
|
||||
@ -3113,8 +3107,4 @@ books_series_link feeds
|
||||
s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,))
|
||||
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
|
||||
|
||||
|
||||
|
@ -582,4 +582,22 @@ class SchemaUpgrade(object):
|
||||
# statements
|
||||
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)
|
||||
|
||||
|
||||
|
@ -666,15 +666,13 @@ class BrowseServer(object):
|
||||
if add_category_links:
|
||||
added_key = False
|
||||
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']:
|
||||
categories = mi.get(key)
|
||||
if isinstance(categories, basestring):
|
||||
categories = [categories]
|
||||
dbtags = []
|
||||
for category in categories:
|
||||
if category not in ccache:
|
||||
continue
|
||||
dbtag = None
|
||||
for tag in ccache[key]:
|
||||
if tag.name == category:
|
||||
|
@ -12,6 +12,34 @@ from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.config import tweaks
|
||||
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):
|
||||
fmt = fmt.lower()
|
||||
if fmt == 'jpeg':
|
||||
@ -19,7 +47,7 @@ def normalize_format_name(fmt):
|
||||
return fmt
|
||||
|
||||
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
|
||||
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).
|
||||
: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 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
|
||||
if isinstance(data, Image):
|
||||
img = data
|
||||
else:
|
||||
img = Image()
|
||||
img.load(data)
|
||||
img = _data_to_image(data)
|
||||
orig_fmt = normalize_format_name(img.format)
|
||||
fmt = os.path.splitext(path)[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:
|
||||
img.size = (resize_to[0], resize_to[1])
|
||||
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():
|
||||
canvas = create_canvas(img.size[0], img.size[1], bgcolor)
|
||||
canvas.compose(img)
|
||||
img = canvas
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
changed = fmt != orig_fmt
|
||||
|
||||
|
@ -10,6 +10,7 @@ from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, \
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre import __appname__, english_sort
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
||||
BeautifulSoup, time, english_sort
|
||||
|
||||
@ -17,6 +18,14 @@ basic_recipes = (BasicNewsRecipe, AutomaticNewsRecipe, CustomIndexRecipe,
|
||||
CalibrePeriodical)
|
||||
_tdir = None
|
||||
_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):
|
||||
'''
|
||||
Compile the code in src and return the first object that is a recipe or profile.
|
||||
|
@ -88,19 +88,89 @@ def serialize_builtin_recipes():
|
||||
def get_builtin_recipe_collection():
|
||||
return etree.parse(P('builtin_recipes.xml')).getroot()
|
||||
|
||||
def get_custom_recipe_collection(db):
|
||||
from calibre.web.feeds.recipes import compile_recipe
|
||||
def get_custom_recipe_collection(*args):
|
||||
from calibre.web.feeds.recipes import compile_recipe, \
|
||||
custom_recipes
|
||||
bdir = os.path.dirname(custom_recipes.file_path)
|
||||
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:
|
||||
recipe = open(recipe, 'rb').read().decode('utf-8')
|
||||
recipe_class = compile_recipe(recipe)
|
||||
if recipe_class is not None:
|
||||
rmap['custom:%d'%id] = recipe_class
|
||||
rmap['custom:%s'%id_] = recipe_class
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
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():
|
||||
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:
|
||||
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 +335,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 +356,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 +368,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):
|
||||
|
@ -9,14 +9,15 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, copy
|
||||
|
||||
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.gui2 import NONE
|
||||
from calibre.utils.localization import get_language
|
||||
from calibre.web.feeds.recipes.collection import \
|
||||
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
|
||||
|
||||
class NewsTreeItem(object):
|
||||
@ -122,26 +123,15 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
||||
LOCATIONS = ['all']
|
||||
searched = pyqtSignal(object)
|
||||
|
||||
def __init__(self, db, *args):
|
||||
def __init__(self, *args):
|
||||
QAbstractItemModel.__init__(self, *args)
|
||||
SearchQueryParser.__init__(self, locations=['all'])
|
||||
self.db = db
|
||||
self.default_icon = QVariant(QIcon(I('news.png')))
|
||||
self.custom_icon = QVariant(QIcon(I('user_profile.png')))
|
||||
self.builtin_recipe_collection = get_builtin_recipe_collection()
|
||||
self.scheduler_config = SchedulerConfig()
|
||||
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):
|
||||
if download:
|
||||
try:
|
||||
@ -158,23 +148,24 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
||||
if recipe.get('id', False) == urn:
|
||||
if coll is self.builtin_recipe_collection:
|
||||
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):
|
||||
self.db.update_feed(int(urn[len('custom:'):]), script, title)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection(self.db)
|
||||
id_ = int(urn[len('custom:'):])
|
||||
update_custom_recipe(id_, title, script)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection()
|
||||
|
||||
def add_custom_recipe(self, title, script):
|
||||
self.db.add_feed(title, script)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection(self.db)
|
||||
add_custom_recipe(title, script)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection()
|
||||
|
||||
def remove_custom_recipes(self, urns):
|
||||
ids = [int(x[len('custom:'):]) for x in urns]
|
||||
self.db.remove_feeds(ids)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection(self.db)
|
||||
for id_ in ids: remove_custom_recipe(id_)
|
||||
self.custom_recipe_collection = get_custom_recipe_collection()
|
||||
|
||||
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):
|
||||
args = list(args)
|
||||
|
Loading…
x
Reference in New Issue
Block a user