KG + GwR revisions

This commit is contained in:
GRiker 2010-06-07 06:44:08 -06:00
commit 045a893d20
42 changed files with 41226 additions and 30593 deletions

View File

@ -4,6 +4,38 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.1
date: 2010-06-04
new features:
- title: "Content server: Add option to control category groupiong in OPDS feeds"
- title: "Make the book details pane occupy the full lower part of the window"
- title: "Add true and false searches for date based columns"
tickets: [5717]
bug fixes:
- title: "iPad driver: Various bug fixes."
- title: "SONY driver: Fix Launcher partition being detected as storage card in linux"
- title: "Fix news downloading breaking on windows systems with local encoding other than UTF-8."
- title: "SONY driver: Fix problem caused by null titles"
- title: "Make the new splash screen not always stay on top"
tickets: [5700]
- title: "When setting an image with transparent pixels as the book cover, overlay it on a white background first. Fixes transparent covers getting random backgrounds."
- title: "Content server: Fix stanza integration when entering the server URL my hand"
improved recipes:
- Gizmodo
- Vreme
- version: 0.7.0
date: 2010-06-04

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1,25 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1275798572(BasicNewsRecipe):
title = u'CBC Canada'
publisher = 'www.cbc.ca'
language = 'en_CA'
__author__ = 'rty'
category = 'news'
oldest_article = 4
max_articles_per_feed = 100
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
masthead_url = 'http://www.cbc.ca/includes/gfx/cbcnews_logo_09.gif'
cover_url = 'http://img692.imageshack.us/img692/2814/cbc.png'
keep_only_tags = [dict(name='div', attrs={'id':['storyhead','storybody']})]
remove_tags_after = dict(id=['socialtools'])
feeds = [(u'Top Stories', u'http://rss.cbc.ca/lineup/topstories.xml'),
(u'World', u'http://rss.cbc.ca/lineup/world.xml'),
(u'National', u'http://rss.cbc.ca/lineup/canada.xml'),
(u'Manitoba', u'http://rss.cbc.ca/lineup/canada-manitoba.xml'),
(u'Politics', u'http://rss.cbc.ca/lineup/politics.xml'),
(u'Tech & Science', u'http://rss.cbc.ca/lineup/technology.xml'),
(u'Books', u'http://rss.cbc.ca/lineup/arts-books.xml')]

View File

@ -0,0 +1,57 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
haaretz.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Haaretz_en(BasicNewsRecipe):
title = 'Haaretz in English'
__author__ = 'Darko Miletic'
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
publisher = 'haaretz.com'
category = 'news, politics, Israel'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
use_embedded_content = False
language = 'en_IL'
publication_type = 'newspaper'
remove_empty_feeds = True
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
remove_tags_before = dict(name='h1')
remove_tags_after = dict(attrs={'id':'innerArticle'})
keep_only_tags = [dict(attrs={'id':'content'})]
feeds = [
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -41,6 +41,8 @@ mimetypes.add_type('application/vnd.palm', '.pdb')
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
mimetypes.add_type('application/x-cbz', '.cbz')
mimetypes.add_type('application/x-cbr', '.cbr')
mimetypes.add_type('image/wmf', '.wmf')
guess_type = mimetypes.guess_type
import cssutils

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.0'
__version__ = '0.7.1'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -1163,7 +1163,7 @@ class ITUNES(DevicePlugin):
hits = lib_books.Search(cached_book['author'],SearchField.index('Artists'))
if hits:
for hit in hits:
self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
#self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
if hit.Name == cached_book['title']:
self.log.info(" matched '%s' by %s" % (hit.Name, hit.Artist))
return hit
@ -1359,7 +1359,6 @@ class ITUNES(DevicePlugin):
if DEBUG:
self.log.info('ITUNES._get_library_books():\n No Books playlist')
elif iswindows:
lib = None
try:
@ -1466,7 +1465,7 @@ class ITUNES(DevicePlugin):
cmd = "defaults read com.apple.itunes NSNavLastRootDirectory"
proc = subprocess.Popen( cmd, shell=True, cwd=os.curdir, stdout=subprocess.PIPE)
retcode = proc.wait()
media_dir = proc.communicate()[0].strip()
media_dir = os.path.abspath(proc.communicate()[0].strip())
if os.path.exists(media_dir):
self.iTunes_media = media_dir
else:
@ -1493,12 +1492,13 @@ class ITUNES(DevicePlugin):
soup = BeautifulSoup(xml.read().decode('utf-8'))
mf = soup.find('key',text="Music Folder").parent
string = mf.findNext('string').renderContents()
media_dir = string[len('file://localhost/'):].replace('%20',' ')
media_dir = os.path.abspath(string[len('file://localhost/'):].replace('%20',' '))
if os.path.exists(media_dir):
self.iTunes_media = media_dir
else:
self.log.error(" could not extract valid iTunes.media_dir from %s" % self.iTunes.LibraryXMLPath)
self.log.error(" %s" % string.parent.prettify())
self.log.error(" '%s' not found" % media_dir)
if DEBUG:
self.log.info( " [%s - %s (%s), driver version %d.%d.%d]" %
@ -1514,13 +1514,7 @@ class ITUNES(DevicePlugin):
'''
if isosx:
storage_path = os.path.split(cached_book['lib_book'].location().path)
presumptive_path = os.path.join(self.iTunes_media,
'iTunes Music',
cached_book['author'][0],
cached_book['title'],
storage_path[1])
if os.path.exists(presumptive_path):
if cached_book['lib_book'].location().path.startswith(self.iTunes_media):
title_storage_path = storage_path[0]
if DEBUG:
self.log.info("ITUNES._remove_from_iTunes():")
@ -1545,7 +1539,7 @@ class ITUNES(DevicePlugin):
self.log.info(" author_storage_path not empty (%d objects):" % len(author_files))
self.log.info(" %s" % '\n'.join(author_files))
else:
self.log.info(" '%s' not found in iTunes storage, no files deleted" % presumptive_path)
self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title'])
self.iTunes.delete(cached_book['lib_book'])
@ -1560,15 +1554,7 @@ class ITUNES(DevicePlugin):
if book:
path = book.Location
storage_path = os.path.split(book.Location)
# This assumes that 'Books' will be the storage subdirectory in
# all versions of Windows. The XML file doesn't offer any deeper information
# than the media directory.
presumptive_path = os.path.join(self.iTunes_media,
'Books',
cached_book['author'],
storage_path[1])
if os.path.exists(presumptive_path):
if book.Location.startswith(self.iTunes_media):
if DEBUG:
self.log.info("ITUNES._remove_from_iTunes():")
self.log.info(" removing '%s' at %s" %
@ -1585,7 +1571,7 @@ class ITUNES(DevicePlugin):
# Delete from iTunes database
else:
self.log.info(" '%s' not in iTunes storage, no files deleted" % presumptive_path)
self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title'])
book.Delete()

View File

@ -445,6 +445,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.username.setText(opts.username)
self.password.setText(opts.password if opts.password else '')
self.opt_max_opds_items.setValue(opts.max_opds_items)
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
self.auto_launch.setChecked(config['autolaunch_server'])
self.systray_icon.setChecked(config['systray_icon'])
self.sync_news.setChecked(config['upload_news_to_device'])
@ -848,6 +849,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
sc.set('port', self.port.value())
sc.set('max_cover', mcs)
sc.set('max_opds_items', self.opt_max_opds_items.value())
sc.set('max_opds_ungrouped_items',
self.opt_max_opds_ungrouped_items.value())
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()

View File

@ -892,6 +892,26 @@
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
<property name="minimum">
<number>25</number>
</property>
<property name="maximum">
<number>1000000</number>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Max. OPDS &amp;ungrouped items:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_ungrouped_items</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>

View File

@ -213,7 +213,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.endInsertRows()
self.count_changed()
def search(self, text, refinement, reset=True):
def search(self, text, reset=True):
try:
self.db.search(text)
except ParseException:
@ -224,9 +224,10 @@ class BooksModel(QAbstractTableModel): # {{{
self.clear_caches()
self.reset()
if self.last_search:
# Do not issue search done for the null search. It is used to clear
# the search and count records for restrictions
self.searched.emit(True)
def sort(self, col, order, reset=True):
if not self.db:
return
@ -257,7 +258,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.sort(col, self.sorted_on[1], reset=reset)
def research(self, reset=True):
self.search(self.last_search, False, reset=reset)
self.search(self.last_search, reset=reset)
def columnCount(self, parent):
if parent and parent.isValid():
@ -730,6 +731,8 @@ class BooksModel(QAbstractTableModel): # {{{
def set_search_restriction(self, s):
self.db.data.set_search_restriction(s)
self.search('')
return self.rowCount(None)
# }}}
@ -874,7 +877,7 @@ class DeviceBooksModel(BooksModel): # {{{
return flags
def search(self, text, refinement, reset=True):
def search(self, text, reset=True):
if not text or not text.strip():
self.map = list(range(len(self.db)))
else:
@ -1086,7 +1089,6 @@ class DeviceBooksModel(BooksModel): # {{{
idx = self.map[row]
if cname == 'title' :
self.db[idx].title = val
self.db[idx].title_sorter = val
elif cname == 'authors':
self.db[idx].authors = string_to_authors(val)
elif cname == 'collections':

View File

@ -437,10 +437,6 @@ class BooksView(QTableView): # {{{
self._search_done = search_done
self._model.searched.connect(self.search_done)
def connect_to_restriction_set(self, tv):
# must be synchronous (not queued)
tv.restriction_set.connect(self._model.set_search_restriction)
def connect_to_book_display(self, bd):
self._model.new_bookdisplay_data.connect(bd)

View File

@ -152,7 +152,7 @@ class Main(MainWindow, Ui_MainWindow):
self.stack.setCurrentIndex(1)
self.renderer.start()
def find(self, search, refinement):
def find(self, search):
self.last_search = search
try:
self.document.search(search)

View File

@ -57,7 +57,7 @@ class SearchBox2(QComboBox):
INTERVAL = 1500 #: Time to wait before emitting search signal
MAX_COUNT = 25
search = pyqtSignal(object, object)
search = pyqtSignal(object)
def __init__(self, parent=None):
QComboBox.__init__(self, parent)
@ -97,8 +97,12 @@ class SearchBox2(QComboBox):
self.help_state = False
def clear_to_help(self):
self.search.emit('')
self._in_a_search = False
self.setEditText(self.help_text)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.line_edit.home(False)
self.help_state = True
self.line_edit.setStyleSheet(
@ -111,7 +115,6 @@ class SearchBox2(QComboBox):
def clear(self):
self.clear_to_help()
self.search.emit('', False)
def search_done(self, ok):
if not unicode(self.currentText()).strip():
@ -155,9 +158,8 @@ class SearchBox2(QComboBox):
if not text or text == self.help_text:
return self.clear()
self.help_state = False
refinement = text.startswith(self.prev_search) and ':' not in text
self.prev_search = text
self.search.emit(text, refinement)
self.search.emit(text)
idx = self.findText(text, Qt.MatchFixedString)
self.block_signals(True)
@ -187,12 +189,15 @@ class SearchBox2(QComboBox):
self.set_search_string(joiner.join(tags))
def set_search_string(self, txt):
if not txt:
self.clear_to_help()
return
self.normalize_state()
self.setEditText(txt)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.search.emit(txt, False)
self.search.emit(txt)
self.line_edit.end(False)
self.initial_state = False

View File

@ -22,7 +22,6 @@ from calibre.gui2 import error_dialog
class TagsView(QTreeView): # {{{
refresh_required = pyqtSignal()
restriction_set = pyqtSignal(object)
tags_marked = pyqtSignal(object, object)
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
@ -37,24 +36,23 @@ class TagsView(QTreeView): # {{{
self.setIconSize(QSize(30, 30))
self.tag_match = None
def set_database(self, db, tag_match, popularity, restriction):
def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories)
hidden_categories=self.hidden_categories,
search_restriction=None)
self.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match
self.db = db
self.search_restriction = None
self.setModel(self._model)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed)
self.restriction.activated[str].connect(self.search_restriction_set)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False)
def database_changed(self, event, ids):
self.refresh_required.emit()
@ -68,16 +66,12 @@ class TagsView(QTreeView): # {{{
self.model().refresh()
# self.search_restriction_set()
def search_restriction_set(self, s):
self.clear()
if len(s) == 0:
self.search_restriction = ''
def set_search_restriction(self, s):
if s:
self.search_restriction = s
else:
self.search_restriction = 'search:"%s"' % unicode(s).strip()
self.model().set_search_restriction(self.search_restriction)
self.restriction_set.emit(self.search_restriction)
self.recount() # Must happen after the emission of the restriction_set signal
self.tags_marked.emit(self._model.tokens(), self.match_all)
self.search_restriction = None
self.set_new_model()
def mouseReleaseEvent(self, event):
# Swallow everything except leftButton so context menus work correctly
@ -187,21 +181,8 @@ class TagsView(QTreeView): # {{{
return True
def clear(self):
self.model().clear_state()
def saved_searches_changed(self, recount=True):
p = prefs['saved_searches'].keys()
p.sort()
t = self.restriction.currentText()
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
self.restriction.addItem('')
for s in p:
self.restriction.addItem(s)
if t in p: # redo the current restriction, if there was one
self.restriction.setCurrentIndex(self.restriction.findText(t))
self.search_restriction_set(t)
if recount:
self.recount()
if self.model():
self.model().clear_state()
def recount(self, *args):
ci = self.currentIndex()
@ -223,7 +204,8 @@ class TagsView(QTreeView): # {{{
# model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self):
self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories)
hidden_categories=self.hidden_categories,
search_restriction=self.search_restriction)
self.setModel(self._model)
# }}}
@ -311,7 +293,7 @@ class TagTreeItem(object): # {{{
class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent, hidden_categories=None):
def __init__(self, db, parent, hidden_categories=None, search_restriction=None):
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
@ -333,8 +315,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.db = db
self.tags_view = parent
self.hidden_categories = hidden_categories
self.search_restriction = ''
self.ignore_next_search = 0
self.search_restriction = search_restriction
# Reconstruct the user categories, putting them into metadata
tb_cats = self.db.field_metadata
@ -370,9 +351,10 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map = []
self.categories = []
if len(self.search_restriction):
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map,
ids=self.db.search(self.search_restriction, return_matches=True))
if self.search_restriction:
data = self.db.get_categories(sort_on_count=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
@ -385,7 +367,6 @@ class TagsModel(QAbstractItemModel): # {{{
self.category_items[category] = set([tag.name for tag in data[category]])
self.row_map.append(category)
self.categories.append(tb_categories[category]['name'])
return data
def refresh(self):
@ -544,12 +525,6 @@ class TagsModel(QAbstractItemModel): # {{{
def clear_state(self):
self.reset_all_states()
def reinit(self, *args, **kwargs):
if self.ignore_next_search == 0:
self.reset_all_states()
else:
self.ignore_next_search -= 1
def toggle(self, index, exclusive):
if not index.isValid(): return False
item = index.internalPointer()
@ -557,7 +532,6 @@ class TagsModel(QAbstractItemModel): # {{{
item.toggle()
if exclusive:
self.reset_all_states(except_=item.tag)
self.ignore_next_search = 2
self.dataChanged.emit(index, index)
return True
return False

View File

@ -160,9 +160,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.restriction_in_effect = False
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
self.search_clear()
self.search.clear()
self.saved_search.initialize(saved_searches, self.search, colorize=True,
help_text=_('Saved Searches'))
@ -226,14 +226,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
self.connect(self.restore_action, SIGNAL('triggered()'),
self.show_windows)
self.show_windows)
self.connect(self.action_show_book_details,
SIGNAL('triggered(bool)'), self.show_book_info)
SIGNAL('triggered(bool)'), self.show_book_info)
self.connect(self.action_restart, SIGNAL('triggered()'),
self.restart)
self.connect(self.system_tray_icon,
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated)
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated)
self.tool_bar.contextMenuEvent = self.no_op
####################### Start spare job server ########################
@ -521,8 +521,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_done)),
('connect_to_book_display',
(self.status_bar.book_info.show_data,)),
('connect_to_restriction_set',
(self.tags_view,)),
]:
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
getattr(view, func)(*args)
@ -545,24 +543,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
self.search_restriction.activated[str].connect(self.apply_search_restriction)
self.tags_view.set_database(db, self.tag_match, self.popularity)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
self.tags_view.restriction_set.connect(x)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.search.search.connect(self.tags_view.model().reinit)
for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.saved_search, SIGNAL('changed()'),
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
self.saved_searches_changed()
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -585,7 +581,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
########################### Cover Flow ################################
self.cover_flow = None
if CoverFlow is not None:
@ -625,7 +620,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.sidebar.job_done, Qt.QueuedConnection)
if config['autolaunch_server']:
from calibre.library.server.main import start_threaded_server
from calibre.library.server import server_config
@ -683,7 +677,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
d = SavedSearchEditor(self, search)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.saved_searches_changed(recount=True)
self.saved_searches_changed()
self.saved_search.clear_to_help()
def resizeEvent(self, ev):
@ -842,19 +836,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
sm.select(idx, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(idx)
'''
Handling of the count of books in a restricted view requires that
we capture the count after the initial restriction search. To so this,
we require that the restriction_set signal be issued before the search signal,
so that when the search_done happens and the count is displayed,
we can grab the count. This works because the search box is cleared
when a restriction is set, so that first search will find all books.
Adding and deleting books creates another complexity. When added, they are
displayed regardless of whether they match the restriction. However, if they
do not, they are removed at the next search. The counts must take this
Restrictions.
Adding and deleting books creates a complexity. When added, they are
displayed regardless of whether they match a search restriction. However, if
they do not, they are removed at the next search. The counts must take this
behavior into effect.
'''
@ -862,15 +848,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
self.restriction_count_of_books_in_library = c
if self.restriction_in_effect:
self.set_number_of_books_shown(compute_count=False)
self.set_number_of_books_shown()
def mark_restriction_set(self, r):
self.restriction_in_effect = False if r is None or not r else True
def apply_search_restriction(self, r):
r = unicode(r)
if r is not None and r != '':
self.restriction_in_effect = True
restriction = "search:%s"%(r)
else:
self.restriction_in_effect = False
restriction = ''
self.restriction_count_of_books_in_view = \
self.library_view.model().set_search_restriction(restriction)
self.search.clear_to_help()
self.saved_search.clear_to_help()
self.tags_view.set_search_restriction(restriction)
self.set_number_of_books_shown()
def set_number_of_books_shown(self, compute_count):
def set_number_of_books_shown(self):
if self.current_view() == self.library_view and self.restriction_in_effect:
if compute_count:
self.restriction_count_of_books_in_view = self.current_view().row_count()
t = _("({0} of {1})").format(self.current_view().row_count(),
self.restriction_count_of_books_in_view)
self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }')
@ -884,18 +880,31 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_count.setText(t)
def search_box_cleared(self):
self.set_number_of_books_shown(compute_count=True)
self.tags_view.clear()
self.saved_search.clear_to_help()
def search_clear(self):
self.set_number_of_books_shown(compute_count=True)
self.search.clear()
self.set_number_of_books_shown()
def search_done(self, view, ok):
if view is self.current_view():
self.search.search_done(ok)
self.set_number_of_books_shown(compute_count=False)
self.set_number_of_books_shown()
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
self.search_restriction.addItem('')
self.tags_view.recount()
for s in p:
self.search_restriction.addItem(s)
if t:
if t in p: # redo the current restriction, if there was one
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
# self.tags_view.set_search_restriction(t)
else:
self.search_restriction.setCurrentIndex(0)
self.apply_search_restriction('')
def sync_cf_to_listview(self, current, previous):
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
@ -2304,14 +2313,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def library_moved(self, newloc):
if newloc is None: return
db = LibraryDatabase2(newloc)
self.library_path = newloc
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.tags_view.set_database(db, self.tag_match, self.popularity)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clearMessage()
self.search.clear_to_help()
self.status_bar.reset_info()
self.library_view.model().count_changed()
prefs['library_path'] = self.library_path
############################################################################
@ -2358,7 +2370,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_restriction.setEnabled(False)
for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(False)
self.set_number_of_books_shown(compute_count=False)
self.set_number_of_books_shown()
def device_job_exception(self, job):

View File

@ -424,7 +424,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.set_bookmarks(self.iterator.bookmarks)
def find(self, text, refinement, repeat=False, backwards=False):
def find(self, text, repeat=False, backwards=False):
if not text:
self.view.search('')
return self.search.search_done(False)

View File

@ -241,6 +241,24 @@ class ResultCache(SearchQueryParser):
matches = set([])
if len(query) < 2:
return matches
if location == 'date':
location = 'timestamp'
loc = self.field_metadata[location]['rec_index']
if query == 'false':
for item in self._data:
if item is None: continue
if item[loc] is None or item[loc] == UNDEFINED_DATE:
matches.add(item[0])
return matches
if query == 'true':
for item in self._data:
if item is None: continue
if item[loc] is not None and item[loc] != UNDEFINED_DATE:
matches.add(item[0])
return matches
relop = None
for k in self.date_search_relops.keys():
if query.startswith(k):
@ -249,10 +267,6 @@ class ResultCache(SearchQueryParser):
if relop is None:
(p, relop) = self.date_search_relops['=']
if location == 'date':
location = 'timestamp'
loc = self.field_metadata[location]['rec_index']
if query == _('today'):
qd = now()
field_count = 3
@ -301,7 +315,7 @@ class ResultCache(SearchQueryParser):
if query == 'false':
query = '0'
elif query == 'true':
query = '>0'
query = '!=0'
relop = None
for k in self.numeric_search_relops.keys():
if query.startswith(k):

View File

@ -38,6 +38,12 @@ def server_config(defaults=None):
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
help=_('The maximum number of matches to return per OPDS query. '
'This affects Stanza, WordPlayer, etc. integration.'))
c.add_opt('max_opds_ungrouped_items', ['--max-opds-ungrouped-items'],
default=100,
help=_('Group items in categories such as author/tags '
'by first letter when there are more than this number '
'of items. Default: %default. Set to a large number '
'to disable grouping.'))
return c
def main():

View File

@ -445,7 +445,7 @@ class OPDSServer(object):
id_ = 'calibre-category-feed:'+which
MAX_ITEMS = 50
MAX_ITEMS = self.opts.max_opds_ungrouped_items
if len(items) <= MAX_ITEMS:
max_items = self.opts.max_opds_items
@ -459,8 +459,6 @@ class OPDSServer(object):
self.text, self.count = text, count
starts = set([x.name[0] for x in items])
if len(starts) > MAX_ITEMS:
starts = set([x.name[:2] for x in items])
category_groups = OrderedDict()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if

View File

@ -8,16 +8,25 @@ Customizing |app|
==================================
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn how to
use *plugins* to customize and control various aspects of |app|'s behavior.
Theer are different kinds of plugins, corresponding to different aspects of |app|. As more and more aspects of |app|
are modularized, new plugin types will be added.
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
use *plugins* to add funtionality to |app|.
.. contents::
:depth: 2
:local:
Environment variables
-----------------------
* ``CALIBRE_CONFIG_DIRECTORY``
* ``CALIBRE_OVERRIDE_DATABASE_PATH``
* ``CALIBRE_DEVELOP_FROM``
* ``CALIBRE_OVERRIDE_LANG``
* ``SYSFS_PATH``
* ``http_proxy``
A Hello World plugin
------------------------

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -171,8 +171,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
border_color)
compose_image(canvas, img, left, top)
p.DestroyMagickWand(img)
with open(path_to_image, 'wb') as f:
p.MagickWriteImage(canvas, f)
p.MagickWriteImage(canvas,path_to_image)
p.DestroyMagickWand(canvas)
def create_cover_page(top_lines, logo_path, width=590, height=750,

View File

@ -24,6 +24,7 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.web.feeds import feed_from_xml, templates, feeds_from_index, Feed
from calibre.web.fetch.simple import option_parser as web2disk_option_parser
from calibre.web.fetch.simple import RecursiveFetcher
from calibre.utils.magick_draw import add_borders_to_image
from calibre.utils.threadpool import WorkRequest, ThreadPool, NoResultsPending
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import now as nowf
@ -283,6 +284,15 @@ class BasicNewsRecipe(Recipe):
#: Override this in your recipe to provide a url to use as a masthead.
masthead_url = None
#: By default, the cover image returned by get_cover_url() will be used as
#: the cover for the periodical. Overriding this in your recipe instructs
#: calibre to render the downloaded cover into a frame whose width and height
#: are expressed as a percentage of the downloaded cover.
#: cover_margins = (10,15,'white') pads the cover with a white margin
#: 10px on the left and right, 15px on the top and bottom.
#: Colors name defined at http://www.imagemagick.org/script/color.php
cover_margins = (0,0,'white')
#: Set to a non empty string to disable this recipe
#: The string will be used as the disabled message
recipe_disabled = None
@ -974,6 +984,11 @@ class BasicNewsRecipe(Recipe):
self.report_progress(1, _('Downloading cover from %s')%cu)
with nested(open(cpath, 'wb'), closing(self.browser.open(cu))) as (cfile, r):
cfile.write(r.read())
if self.cover_margins[0] or self.cover_margins[1]:
add_borders_to_image(cpath,
left=self.cover_margins[0],right=self.cover_margins[0],
top=self.cover_margins[1],bottom=self.cover_margins[1],
border_color=self.cover_margins[2])
if ext.lower() == 'pdf':
from calibre.ebooks.metadata.pdf import get_metadata
stream = open(cpath, 'rb')

View File

@ -241,7 +241,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
results.add(urn)
return results
def search(self, query, refinement):
def search(self, query):
try:
results = self.parse(unicode(query))
if not results: