diff --git a/recipes/fhm_uk.recipe b/recipes/fhm_uk.recipe index 07f2b4b64e..6ee5ae3fb6 100644 --- a/recipes/fhm_uk.recipe +++ b/recipes/fhm_uk.recipe @@ -2,19 +2,19 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1325006965(BasicNewsRecipe): title = u'FHM UK' - description = 'Good News for Men' + description = 'Good News for Men.' cover_url = 'http://www.greatmagazines.co.uk/covers/large/w197/current/fhm.jpg' # cover_url = 'http://profile.ak.fbcdn.net/hprofile-ak-snc4/373529_38324934806_64930243_n.jpg' masthead_url = 'http://www.fhm.com/App_Resources/Images/Site/re-design/logo.gif' __author__ = 'Dave Asbury' - # last updated 14/4/12 + # last updated 1/7/12 language = 'en_GB' oldest_article = 28 - max_articles_per_feed = 12 + max_articles_per_feed = 8 remove_empty_feeds = True no_stylesheets = True #auto_cleanup = True - #articles_are_obfuscated = True + # articles_are_obfuscated = True keep_only_tags = [ dict(name='h1'), dict(name='img',attrs={'id' : 'ctl00_Body_imgMainImage'}), @@ -28,11 +28,18 @@ class AdvancedUserRecipe1325006965(BasicNewsRecipe): #] feeds = [ - (u'From the Homepage',u'http://feed43.com/0032328550253453.xml'), - #http://feed43.com/8053226782885416.xml'), - (u'Funny - The Very Best Of The Internet',u'http://feed43.com/4538510106331565.xml'), - (u'Upgrade',u'http://feed43.com/0877305847443234.xml'), - #(u'The Final Countdown', u'http://feed43.com/3576106158530118.xml'), - #(u'Gaming',u'http://feed43.com/0755006465351035.xml'), - (u'Gaming',u'http://feed43.com/6537162612465672.xml'), + (u'Homepage 1',u'http://feed43.com/6655867614547036.xml'), + (u'Homepage 2',u'http://feed43.com/4167731873103110.xml'), + (u'Homepage 3',u'http://feed43.com/7667138788771570.xml'), + (u'Homepage 4',u'http://feed43.com/6550421522527341.xml'), + (u'Funny - The Very Best Of The Internet',u'http://feed43.com/4538510106331565.xml'), + (u'Gaming',u'http://feed43.com/6537162612465672.xml'), + (u'Girls',u'http://feed43.com/3674777224513254.xml'), ] + + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' diff --git a/recipes/heraldo.recipe b/recipes/heraldo.recipe index f3236ec4a9..b00d3f23c8 100644 --- a/recipes/heraldo.recipe +++ b/recipes/heraldo.recipe @@ -3,8 +3,8 @@ __license__ = 'GPL v3' __copyright__ = '04 December 2010, desUBIKado' __author__ = 'desUBIKado' __description__ = 'Daily newspaper from Aragon' -__version__ = 'v0.04' -__date__ = '6, Januery 2011' +__version__ = 'v0.05' +__date__ = '5, Februery 2012' ''' [url]http://www.heraldo.es/[/url] ''' @@ -38,7 +38,7 @@ class heraldo(BasicNewsRecipe): keep_only_tags = [dict(name='div', attrs={'id':['dts','com']})] remove_tags = [dict(name='a', attrs={'class':['com flo-r','enl-if','enl-df']}), - dict(name='div', attrs={'class':['brb-b-s con marg-btt','cnt-rel con']}), + dict(name='div', attrs={'class':['brb-b-s con marg-btt','cnt-rel con','col5-f1']}), dict(name='form', attrs={'class':'form'}), dict(name='ul', attrs={'id':['cont-tags','pag-1']})] @@ -72,6 +72,9 @@ class heraldo(BasicNewsRecipe): preprocess_regexps = [ -# To separate the comments with a blank line +# Para separar los comentarios con una linea en blanco (re.compile(r'
\n') - path0 = chapters[0][1] - subpath = os.path.dirname(path0) + with open(htmlpath, 'wb') as f: + if chapters: + f.write('\n') + path0 = chapters[0][1] + subpath = os.path.dirname(path0) + base = os.path.dirname(f.name) - for chapter in chapters: - title = chapter[0] - rsrcname = os.path.basename(chapter[1]) - rsrcpath = os.path.join(subpath, rsrcname) - # title should already be url encoded - url = "
" + title + " \n" - if isinstance(url, unicode): - url = url.encode('utf-8') - f.write(url) + for chapter in chapters: + title = chapter[0] + rsrcname = os.path.basename(chapter[1]) + rsrcpath = os.path.join(subpath, rsrcname) + if (not os.path.exists(os.path.join(base, rsrcpath)) and + os.path.exists(os.path.join(base, chapter[1]))): + rsrcpath = chapter[1] - f.write("") - else: - f.write(hhcdata) - f.close() + # title should already be url encoded + url = "
" + title + " \n" + if isinstance(url, unicode): + url = url.encode('utf-8') + f.write(url) + + f.write("") + else: + f.write(hhcdata) return htmlpath diff --git a/src/calibre/ebooks/metadata/sources/ozon.py b/src/calibre/ebooks/metadata/sources/ozon.py index 3845ebf97b..ebb104818f 100644 --- a/src/calibre/ebooks/metadata/sources/ozon.py +++ b/src/calibre/ebooks/metadata/sources/ozon.py @@ -54,30 +54,35 @@ class Ozon(Source): # for ozon.ru search we have to format ISBN with '-' isbn = _format_isbn(log, identifiers.get('isbn', None)) - # TODO: format isbn! - qItems = set([isbn, title]) - if authors: - qItems |= frozenset(authors) - qItems.discard(None) - qItems.discard('') - qItems = map(_quoteString, qItems) - - q = u' '.join(qItems).strip() - log.info(u'search string: ' + q) - - if isinstance(q, unicode): - q = q.encode('utf-8') - if not q: - return None - - search_url += quote_plus(q) + ozonid = identifiers.get('ozon', None) + + unk = unicode(_('Unknown')).upper() + if (title and title != unk) or (authors and authors != [unk]) or isbn or not ozonid: + qItems = set([isbn, title]) + if authors: + qItems |= frozenset(authors) + qItems.discard(None) + qItems.discard('') + qItems = map(_quoteString, qItems) + + q = u' '.join(qItems).strip() + log.info(u'search string: ' + q) + + if isinstance(q, unicode): + q = q.encode('utf-8') + if not q: + return None + + search_url += quote_plus(q) + else: + search_url = self.ozon_url + '/webservices/OzonWebSvc.asmx/ItemDetail?ID=%s' % ozonid + log.debug(u'search url: %r'%search_url) - return search_url # }}} def identify(self, log, result_queue, abort, title=None, authors=None, - identifiers={}, timeout=30): # {{{ + identifiers={}, timeout=60): # {{{ from lxml import etree from calibre.ebooks.chardet import xml_to_unicode @@ -99,7 +104,7 @@ class Ozon(Source): try: parser = etree.XMLParser(recover=True, no_network=True) feed = etree.fromstring(xml_to_unicode(raw, strip_encoding_pats=True, assume_utf8=True)[0], parser=parser) - entries = feed.xpath('//*[local-name() = "SearchItems"]') + entries = feed.xpath('//*[local-name()="SearchItems" or local-name()="ItemDetail"]') if entries: metadata = self.get_metadata(log, entries, title, authors, identifiers) self.get_all_details(log, metadata, abort, result_queue, identifiers, timeout) @@ -112,8 +117,8 @@ class Ozon(Source): def get_metadata(self, log, entries, title, authors, identifiers): # {{{ # some book titles have extra characters like this # TODO: make a twick - reRemoveFromTitle = None - #reRemoveFromTitle = re.compile(r'[?!:.,;+-/&%"\'=]') + #reRemoveFromTitle = None + reRemoveFromTitle = re.compile(r'[?!:.,;+-/&%"\'=]') title = unicode(title).upper() if title else '' if reRemoveFromTitle: @@ -163,7 +168,7 @@ class Ozon(Source): metadata.append(mi) #log.debug(u'added metadata %s %s.'%(mi.title, mi.authors)) else: - log.debug(u'skipped metadata %s %s. (does not match the query)'%(mi.title, mi.authors)) + log.debug(u'skipped metadata %s %s. (does not match the query)'%(unicode(mi.title), mi.authors)) return metadata # }}} @@ -301,7 +306,7 @@ class Ozon(Source): if series: metadata.series = series - xpt = u'normalize-space(substring-after(//meta[@name="description"]/@content, "ISBN"))' + xpt = u'normalize-space(//*[@class="product-detail"]//text()[starts-with(., "ISBN")])' isbn_str = doc.xpath(xpt) if isbn_str: all_isbns = [check_isbn(isbn) for isbn in self.isbnRegex.findall(isbn_str) if _verifyISBNIntegrity(log, isbn)] @@ -326,7 +331,7 @@ class Ozon(Source): # can be set before from xml search responce if not metadata.pubdate: - xpt = u'normalize-space(//div[@class="product-misc"]//text()[contains(., "г.")])' + xpt = u'normalize-space(substring-after(//div[@class="product-detail"]//text()[contains(., "г.")],";"))' yearIn = doc.xpath(xpt) if yearIn: matcher = re.search(r'\d{4}', yearIn) @@ -334,17 +339,20 @@ class Ozon(Source): metadata.pubdate = toPubdate(log, matcher.group(0)) # overwrite comments from HTML if any - xpt = u'//table[@id="detail_description"]//tr/td' + xpt = u'//*[@id="detail_description"]//*[contains(text(), "От производителя")]/../node()[not(self::comment())][not(self::br)][preceding::*[contains(text(), "От производителя")]]' + from lxml.etree import ElementBase comment_elem = doc.xpath(xpt) if comment_elem: - comments = unicode(etree.tostring(comment_elem[0], encoding=unicode)) - if comments: - # cleanup root tag, TODO: remove tags like object/embeded - comments = re.sub(ur'\A.*?|.*\Z', u'', comments.strip(), re.MULTILINE).strip() - if comments and (not metadata.comments or len(comments) > len(metadata.comments)): - metadata.comments = comments - else: - log.debug('HTML book description skipped in favour of search service xml responce') + comments = u'' + for node in comment_elem: + if isinstance(node, ElementBase): + comments += unicode(etree.tostring(node, encoding=unicode)) + elif isinstance(node, basestring) and node.strip(): + comments += unicode(node) + u'\n' + if comments and (not metadata.comments or len(comments) > len(metadata.comments)): + metadata.comments = comments + else: + log.debug('HTML book description skipped in favour of search service xml responce') else: log.debug('No book description found in HTML') # }}} @@ -430,7 +438,8 @@ def _translageLanguageToCode(displayLang): # {{{ u'Китайский': 'zh', u'Японский': 'ja', u'Финский' : 'fi', - u'Польский' : 'pl',} + u'Польский' : 'pl', + u'Украинский' : 'uk',} return langTbl.get(displayLang, None) # }}} @@ -454,7 +463,7 @@ def toPubdate(log, yearAsString): # {{{ res = None if yearAsString: try: - res = parse_only_date(yearAsString) + res = parse_only_date(u"01.01." + yearAsString) except: log.error('cannot parse to date %s'%yearAsString) return res diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index ff750ff3ba..695ca3af7f 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -66,6 +66,7 @@ class PagedDisplay this.in_paged_mode = false this.current_margin_side = 0 this.is_full_screen_layout = false + this.max_col_width = -1 set_geometry: (cols_per_screen=1, margin_top=20, margin_side=40, margin_bottom=20) -> this.margin_top = margin_top @@ -108,6 +109,11 @@ class PagedDisplay # Minimum column width, for the cases when the window is too # narrow col_width = Math.max(100, ((ww - adjust)/n) - 2*sm) + if this.max_col_width > 0 and col_width > this.max_col_width + # Increase the side margin to ensure that col_width is no larger + # than max_col_width + sm += Math.ceil( (col_width - this.max_col_width) / 2*n ) + col_width = Math.max(100, ((ww - adjust)/n) - 2*sm) this.page_width = col_width + 2*sm this.screen_width = this.page_width * this.cols_per_screen @@ -360,5 +366,4 @@ if window? # TODO: # Resizing of images -# Full screen mode # Highlight on jump_to_anchor diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 5947087535..8fcddc7080 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -13,7 +13,7 @@ from lxml import etree from calibre import guess_type, strftime from calibre.ebooks.BeautifulSoup import BeautifulSoup -from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML +from calibre.ebooks.oeb.base import XPath, XHTML_NS, XHTML, xml2text, urldefrag from calibre.library.comments import comments_to_html from calibre.utils.date import is_date_undefined from calibre.ebooks.chardet import strip_encoding_declarations @@ -41,11 +41,25 @@ class Jacket(object): return removed def remove_first_image(self): + deleted_item = None for item in self.oeb.spine: removed = self.remove_images(item) if removed > 0: self.log('Removed first image') + body = XPath('//h:body')(item.data) + if body: + raw = xml2text(body[0]).strip() + imgs = XPath('//h:img|//svg:svg')(item.data) + if not raw and not imgs: + self.log('Removing %s as it has no content'%item.href) + self.oeb.manifest.remove(item) + deleted_item = item break + if deleted_item is not None: + for item in list(self.oeb.toc): + href = urldefrag(item.href)[0] + if href == deleted_item.href: + self.oeb.toc.remove(item) def insert_metadata(self, mi): self.log('Inserting metadata into book...') diff --git a/src/calibre/gui2/complete.py b/src/calibre/gui2/complete.py index fb1f39dfa3..947493cbb9 100644 --- a/src/calibre/gui2/complete.py +++ b/src/calibre/gui2/complete.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import (QLineEdit, QAbstractListModel, Qt, - QApplication, QCompleter, pyqtSignal) + QApplication, QCompleter) from calibre.utils.icu import sort_key, lower from calibre.gui2 import NONE @@ -56,7 +56,7 @@ class MultiCompleteLineEdit(QLineEdit, LineEditECM): to complete non multiple fields as well. ''' - def __init__(self, parent=None): + def __init__(self, parent=None, completer_widget=None): QLineEdit.__init__(self, parent) self.sep = ',' @@ -66,7 +66,7 @@ class MultiCompleteLineEdit(QLineEdit, LineEditECM): self._model = CompleteModel(parent=self) self._completer = c = QCompleter(self._model, self) - c.setWidget(self) + c.setWidget(self if completer_widget is None else completer_widget) c.setCompletionMode(QCompleter.PopupCompletion) c.setCaseSensitivity(Qt.CaseInsensitive) c.setModelSorting(self._model.sorting) @@ -158,21 +158,15 @@ class MultiCompleteLineEdit(QLineEdit, LineEditECM): class MultiCompleteComboBox(EnComboBox): - clear_edit_text = pyqtSignal() - def __init__(self, *args): EnComboBox.__init__(self, *args) - self.setLineEdit(MultiCompleteLineEdit(self)) - # Needed to allow changing the case of an existing item - # otherwise on focus out, the text is changed to the - # item that matches case insensitively - c = self.lineEdit().completer() - c.setCaseSensitivity(Qt.CaseSensitive) - self.dummy_model = CompleteModel(self) - c.setModel(self.dummy_model) - self.lineEdit()._completer.setWidget(self) - self.clear_edit_text.connect(self.clearEditText, - type=Qt.QueuedConnection) + self.le = MultiCompleteLineEdit(self, completer_widget=self) + self.setLineEdit(self.le) + + def showPopup(self): + c = self.le._completer + c.setCompletionPrefix('') + c.complete() def update_items_cache(self, complete_items): self.lineEdit().update_items_cache(complete_items) @@ -187,18 +181,10 @@ class MultiCompleteComboBox(EnComboBox): self.lineEdit().set_add_separator(what) def show_initial_value(self, what): - ''' - Show an initial value. Handle the case of the initial value being blank - correctly (on Qt 4.8.0 having a blank value causes the first value from - the completer to be shown, when the event loop runs). - ''' - what = unicode(what) + what = unicode(what) if what else u'' le = self.lineEdit() - if not what.strip(): - self.clear_edit_text.emit() - else: - self.setEditText(what) - le.selectAll() + self.setEditText(what) + le.selectAll() if __name__ == '__main__': from PyQt4.Qt import QDialog, QVBoxLayout @@ -207,5 +193,8 @@ if __name__ == '__main__': d.setLayout(QVBoxLayout()) le = MultiCompleteComboBox(d) d.layout().addWidget(le) - le.all_items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', 'oothree'] + items = ['one', 'otwo', 'othree', 'ooone', 'ootwo', + 'oothree'] + le.update_items_cache(items) + le.show_initial_value('') d.exec_() diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index ba2fa0713e..6d43abdf63 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -12,8 +12,8 @@ from PyQt4.Qt import QPixmap, SIGNAL from calibre.gui2 import choose_images, error_dialog from calibre.gui2.convert.metadata_ui import Ui_Form -from calibre.ebooks.metadata import (authors_to_string, string_to_authors, - MetaInformation, title_sort) +from calibre.ebooks.metadata import (string_to_authors, MetaInformation, + title_sort) from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.convert import Widget @@ -74,14 +74,12 @@ class MetadataWidget(Widget, Ui_Form): mi = self.db.get_metadata(self.book_id, index_is_id=True) self.title.setText(mi.title) - if mi.publisher: - self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher)) + self.publisher.show_initial_value(mi.publisher if mi.publisher else '') self.author_sort.setText(mi.author_sort if mi.author_sort else '') self.tags.setText(', '.join(mi.tags if mi.tags else [])) self.tags.update_items_cache(self.db.all_tags()) self.comment.html = comments_to_html(mi.comments) if mi.comments else '' - if mi.series: - self.series.setCurrentIndex(self.series.findText(mi.series)) + self.series.show_initial_value(mi.series if mi.series else '') if mi.series_index is not None: try: self.series_index.setValue(mi.series_index) @@ -118,16 +116,11 @@ class MetadataWidget(Widget, Ui_Form): self.author.set_add_separator(tweaks['authors_completer_append_separator']) self.author.update_items_cache(self.db.all_author_names()) - for i in all_authors: - id, name = i - name = authors_to_string([name.strip().replace('|', ',') for n in name.split(',')]) - self.author.addItem(name) - au = self.db.authors(self.book_id, True) if not au: au = _('Unknown') au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')]) - self.author.setEditText(au) + self.author.show_initial_value(au) def initialize_series(self): all_series = self.db.all_series() @@ -135,22 +128,12 @@ class MetadataWidget(Widget, Ui_Form): self.series.set_separator(None) self.series.update_items_cache([x[1] for x in all_series]) - for i in all_series: - id, name = i - self.series.addItem(name) - self.series.setCurrentIndex(-1) - def initialize_publisher(self): all_publishers = self.db.all_publishers() all_publishers.sort(key=lambda x : sort_key(x[1])) self.publisher.set_separator(None) self.publisher.update_items_cache([x[1] for x in all_publishers]) - for i in all_publishers: - id, name = i - self.publisher.addItem(name) - self.publisher.setCurrentIndex(-1) - def get_title_and_authors(self): title = unicode(self.title.text()).strip() if not title: diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 654a9f4b5b..c9c8255076 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -314,14 +314,7 @@ class Text(Base): if self.col_metadata['is_multiple']: self.setter(val) else: - idx = None - for i, c in enumerate(values): - if c == val: - idx = i - self.widgets[1].addItem(c) - self.widgets[1].setEditText('') - if idx is not None: - self.widgets[1].setCurrentIndex(idx) + self.widgets[1].show_initial_value(val) def setter(self, val): if self.col_metadata['is_multiple']: @@ -396,16 +389,8 @@ class Series(Base): self.initial_index = s_index self.initial_val = val val = self.normalize_db_val(val) - idx = None - self.name_widget.clear() - for i, c in enumerate(values): - if c == val: - idx = i - self.name_widget.addItem(c) self.name_widget.update_items_cache(values) - self.name_widget.setEditText('') - if idx is not None: - self.widgets[1].setCurrentIndex(idx) + self.name_widget.show_initial_value(val) def getter(self): n = unicode(self.name_widget.currentText()).strip() @@ -860,8 +845,6 @@ class BulkSeries(BulkBase): self.idx_widget.setChecked(False) self.main_widget.set_separator(None) self.main_widget.update_items_cache(self.all_values) - for c in self.all_values: - self.main_widget.addItem(c) self.main_widget.setEditText('') self.a_c_checkbox.setChecked(False) @@ -1005,15 +988,8 @@ class BulkText(BulkBase): if not self.col_metadata['is_multiple']: val = self.get_initial_value(book_ids) self.initial_val = val = self.normalize_db_val(val) - idx = None self.main_widget.blockSignals(True) - for i, c in enumerate(self.all_values): - if c == val: - idx = i - self.main_widget.addItem(c) - self.main_widget.setEditText('') - if idx is not None: - self.main_widget.setCurrentIndex(idx) + self.main_widget.show_initial_value(val) self.main_widget.blockSignals(False) def commit(self, book_ids, notify=False): diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py index d4990e14d4..218bd90483 100644 --- a/src/calibre/gui2/dialogs/add_empty_book.py +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -6,8 +6,7 @@ __license__ = 'GPL v3' from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ QApplication, QSpinBox, QToolButton, QIcon -from calibre.ebooks.metadata import authors_to_string, string_to_authors -from calibre.utils.icu import sort_key +from calibre.ebooks.metadata import string_to_authors from calibre.gui2.complete import MultiCompleteComboBox from calibre.utils.config import tweaks @@ -56,17 +55,10 @@ class AddEmptyBookDialog(QDialog): self.authors_combo.setEditText(_('Unknown')) def initialize_authors(self, db, author): - all_authors = db.all_authors() - all_authors.sort(key=lambda x : sort_key(x[1])) - for i in all_authors: - id, name = i - name = [name.strip().replace('|', ',') for n in name.split(',')] - self.authors_combo.addItem(authors_to_string(name)) - au = author if not au: au = _('Unknown') - self.authors_combo.setEditText(au.replace('|', ',')) + self.authors_combo.show_initial_value(au.replace('|', ',')) self.authors_combo.set_separator('&') self.authors_combo.set_space_before_sep(True) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index b7af971a63..b8f30f3541 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -261,8 +261,12 @@ class MyBlockingBusy(QDialog): # {{{ else: next = self.db.get_next_series_num_for(series) self.db.set_series(id, series, notify=False, commit=False) - num = next if do_autonumber and series else 1.0 - self.db.set_series_index(id, num, notify=False, commit=False) + if not series: + self.db.set_series_index(id, 1.0, notify=False, commit=False) + elif do_autonumber: # is True if do_series_restart is True + self.db.set_series_index(id, next, notify=False, commit=False) + elif tweaks['series_index_auto_increment'] != 'no_change': + self.db.set_series_index(id, 1.0, notify=False, commit=False) if do_remove_conv: self.db.delete_conversion_options(id, 'PIPE', commit=False) @@ -872,38 +876,25 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): all_authors = self.db.all_authors() all_authors.sort(key=lambda x : sort_key(x[1])) - for i in all_authors: - id, name = i - name = name.strip().replace('|', ',') - self.authors.addItem(name) - self.authors.setEditText('') - self.authors.set_separator('&') self.authors.set_space_before_sep(True) self.authors.set_add_separator(tweaks['authors_completer_append_separator']) self.authors.update_items_cache(self.db.all_author_names()) + self.authors.show_initial_value('') def initialize_series(self): all_series = self.db.all_series() all_series.sort(key=lambda x : sort_key(x[1])) self.series.set_separator(None) self.series.update_items_cache([x[1] for x in all_series]) - - for i in all_series: - id, name = i - self.series.addItem(name) - self.series.setEditText('') + self.series.show_initial_value('') def initialize_publisher(self): all_publishers = self.db.all_publishers() all_publishers.sort(key=lambda x : sort_key(x[1])) self.publisher.set_separator(None) self.publisher.update_items_cache([x[1] for x in all_publishers]) - - for i in all_publishers: - id, name = i - self.publisher.addItem(name) - self.publisher.setEditText('') + self.publisher.show_initial_value('') def tag_editor(self, *args): d = TagEditor(self, self.db, None) diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 8736ae2259..cf63d150e6 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -25,10 +25,6 @@ class SearchDialog(QDialog, Ui_Dialog): all_authors = db.all_authors() all_authors.sort(key=lambda x : sort_key(x[1])) - for i in all_authors: - id, name = i - name = name.strip().replace('|', ',') - self.authors_box.addItem(name) self.authors_box.setEditText('') self.authors_box.set_separator('&') self.authors_box.set_space_before_sep(True) @@ -39,10 +35,7 @@ class SearchDialog(QDialog, Ui_Dialog): all_series.sort(key=lambda x : sort_key(x[1])) self.series_box.set_separator(None) self.series_box.update_items_cache([x[1] for x in all_series]) - for i in all_series: - id, name = i - self.series_box.addItem(name) - self.series_box.setEditText('') + self.series_box.show_initial_value('') all_tags = db.all_tags() self.tags_box.update_items_cache(all_tags) diff --git a/src/calibre/gui2/languages.py b/src/calibre/gui2/languages.py index 0dfbb38b08..f067027097 100644 --- a/src/calibre/gui2/languages.py +++ b/src/calibre/gui2/languages.py @@ -32,8 +32,6 @@ class LanguagesEdit(MultiCompleteComboBox): all_items = sorted(self._lang_map.itervalues(), key=lambda x: (-pmap.get(x, 0), sort_key(x))) self.update_items_cache(all_items) - for item in all_items: - self.addItem(item) @property def vals(self): diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 60b8e3445d..77c3152842 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -125,8 +125,6 @@ class TextDelegate(QStyledItemDelegate): # {{{ editor.set_separator(None) complete_items = [i[1] for i in self.auto_complete_function()] editor.update_items_cache(complete_items) - for item in sorted(complete_items, key=sort_key): - editor.addItem(item) ct = index.data(Qt.DisplayRole).toString() editor.show_initial_value(ct) else: @@ -166,8 +164,6 @@ class CompleteDelegate(QStyledItemDelegate): # {{{ all_items = list(self.db.all_custom( label=self.db.field_metadata.key_to_label(col))) editor.update_items_cache(all_items) - for item in sorted(all_items, key=sort_key): - editor.addItem(item) ct = index.data(Qt.DisplayRole).toString() editor.show_initial_value(ct) else: diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index e0047c2a70..e2706c0a54 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -846,7 +846,9 @@ class BooksModel(QAbstractTableModel): # {{{ s_index = float(match.group(1)) val = pat.sub('', val).strip() elif val: - if tweaks['series_index_auto_increment'] != 'const': + # it is OK to leave s_index == None when using 'no_change' + if tweaks['series_index_auto_increment'] != 'const' and \ + tweaks['series_index_auto_increment'] != 'no_change': s_index = self.db.get_next_cc_series_num_for(val, label=label, num=None) elif typ == 'composite': @@ -915,7 +917,8 @@ class BooksModel(QAbstractTableModel): # {{{ self.db.set_series_index(id, float(match.group(1))) val = pat.sub('', val).strip() elif val: - if tweaks['series_index_auto_increment'] != 'const': + if tweaks['series_index_auto_increment'] != 'const' and \ + tweaks['series_index_auto_increment'] != 'no_change': ni = self.db.get_next_series_num_for(val) if ni != 1: self.db.set_series_index(id, ni) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index f152bf6534..d2a983415f 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -246,14 +246,6 @@ class AuthorsEdit(MultiCompleteComboBox): def initialize(self, db, id_): self.books_to_refresh = set([]) - all_authors = db.all_authors() - all_authors.sort(key=lambda x : sort_key(x[1])) - self.clear() - for i in all_authors: - id, name = i - name = name.strip().replace('|', ',') - self.addItem(name) - self.set_separator('&') self.set_space_before_sep(True) self.set_add_separator(tweaks['authors_completer_append_separator']) @@ -299,7 +291,6 @@ class AuthorsEdit(MultiCompleteComboBox): self.setEditText(' & '.join([x.strip() for x in val])) self.lineEdit().setCursorPosition(0) - return property(fget=fget, fset=fset) def break_cycles(self): @@ -488,19 +479,12 @@ class SeriesEdit(MultiCompleteComboBox): all_series.sort(key=lambda x : sort_key(x[1])) self.update_items_cache([x[1] for x in all_series]) series_id = db.series_id(id_, index_is_id=True) - idx, c = None, 0 - self.clear() + inval = '' for i in all_series: - id, name = i - if id == series_id: - idx = c - self.addItem(name) - c += 1 - - self.lineEdit().setText('') - if idx is not None: - self.setCurrentIndex(idx) - self.original_val = self.current_val + if i[0] == series_id: + inval = i[1] + break + self.original_val = self.current_val = inval def commit(self, db, id_): series = self.current_val @@ -560,7 +544,7 @@ class SeriesIndexEdit(QDoubleSpinBox): return True def increment(self): - if self.db is not None: + if tweaks['series_index_auto_increment'] != 'no_change' and self.db is not None: try: series = self.series_edit.current_val if series and series != self.original_series_name: @@ -1373,17 +1357,12 @@ class PublisherEdit(MultiCompleteComboBox): # {{{ all_publishers.sort(key=lambda x : sort_key(x[1])) self.update_items_cache([x[1] for x in all_publishers]) publisher_id = db.publisher_id(id_, index_is_id=True) - idx = None - self.clear() - for i, x in enumerate(all_publishers): - id_, name = x - if id_ == publisher_id: - idx = i - self.addItem(name) - - self.setEditText('') - if idx is not None: - self.setCurrentIndex(idx) + inval = '' + for pid, name in all_publishers: + if pid == publisher_id: + inval = name + break + self.original_val = self.current_val = inval def commit(self, db, id_): self.books_to_refresh |= db.set_publisher(id_, self.current_val, diff --git a/src/calibre/gui2/store/stores/ebooks_com_plugin.py b/src/calibre/gui2/store/stores/ebooks_com_plugin.py index 656984a86d..7bf6704d9f 100644 --- a/src/calibre/gui2/store/stores/ebooks_com_plugin.py +++ b/src/calibre/gui2/store/stores/ebooks_com_plugin.py @@ -64,11 +64,11 @@ class EbookscomStore(BasicStoreConfig, StorePlugin): continue id = mo.group() - cover_url = ''.join(data.xpath('.//div[@class="img"]//img/@src')) + cover_url = ''.join(data.xpath('.//div[contains(@class, "img")]//img/@src')) title = ''.join(data.xpath( 'descendant::span[@class="book-title"]/a/text()')).strip() - author = ''.join(data.xpath( + author = ', '.join(data.xpath( 'descendant::span[@class="author"]/a/text()')).strip() if not title or not author: continue diff --git a/src/calibre/gui2/store/stores/oreilly_plugin.py b/src/calibre/gui2/store/stores/oreilly_plugin.py deleted file mode 100644 index e45c072eea..0000000000 --- a/src/calibre/gui2/store/stores/oreilly_plugin.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import (unicode_literals, division, absolute_import, print_function) - -__license__ = 'GPL 3' -__copyright__ = '2011, John Schember ' -__docformat__ = 'restructuredtext en' - -import urllib -from contextlib import closing - -from lxml import html - -from PyQt4.Qt import QUrl - -from calibre import browser, url_slash_cleaner -from calibre.gui2 import open_url -from calibre.gui2.store import StorePlugin -from calibre.gui2.store.basic_config import BasicStoreConfig -from calibre.gui2.store.search_result import SearchResult -from calibre.gui2.store.web_store_dialog import WebStoreDialog - -class OReillyStore(BasicStoreConfig, StorePlugin): - - def open(self, parent=None, detail_item=None, external=False): - url = 'http://oreilly.com/ebooks/' - - if external or self.config.get('open_external', False): - open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) - else: - d = WebStoreDialog(self.gui, url, parent, detail_item) - d.setWindowTitle(self.name) - d.set_tags(self.config.get('tags', '')) - d.exec_() - - def search(self, query, max_results=10, timeout=60): - url = 'http://search.oreilly.com/?t1=Books&t2=Format&t3=Ebook&q=' + urllib.quote_plus(query) - - br = browser() - - counter = max_results - with closing(br.open(url, timeout=timeout)) as f: - doc = html.fromstring(f.read()) - for data in doc.xpath('//div[@class="result"]'): - if counter <= 0: - break - - ebook = ' '.join(data.xpath('.//p[@class="note"]/text()')) - if 'ebook' not in ebook.lower(): - continue - - id = ''.join(data.xpath('./div[@class="book_text"]//p[@class="title"]/a/@href')) - - cover_url = ''.join(data.xpath('./a/img[1]/@src')) - - title = ''.join(data.xpath('./div[@class="book_text"]/p[@class="title"]/a/text()')) - author = ''.join(data.xpath('./div[@class="book_text"]/p[@class="note"][1]/text()')) - author = author.split('By ')[-1].strip() - - # Get the detail here because we need to get the ebook id for the detail_item. - with closing(br.open(id, timeout=timeout)) as nf: - idoc = html.fromstring(nf.read()) - - for td in idoc.xpath('//td[@class="optionsTd"]'): - if 'ebook' in ''.join(td.xpath('.//text()')).lower(): - price = ''.join(td.xpath('.//span[@class="price"]/text()')).strip() - formats = ''.join(td.xpath('.//a[@id="availableFormats"]/text()')).strip() - break - - counter -= 1 - - s = SearchResult() - s.cover_url = cover_url.strip() - s.title = title.strip() - s.author = author.strip() - s.detail_item = id.strip() - s.price = price.strip() - s.drm = SearchResult.DRM_UNLOCKED - s.formats = formats.upper() - - yield s diff --git a/src/calibre/gui2/store/stores/ozon_ru_plugin.py b/src/calibre/gui2/store/stores/ozon_ru_plugin.py index 5d977700c8..b54bf01daf 100644 --- a/src/calibre/gui2/store/stores/ozon_ru_plugin.py +++ b/src/calibre/gui2/store/stores/ozon_ru_plugin.py @@ -46,30 +46,37 @@ class OzonRUStore(BasicStoreConfig, StorePlugin): d.set_tags(self.config.get('tags', '')) d.exec_() - - def search(self, query, max_results=10, timeout=60): + def search(self, query, max_results=15, timeout=60): search_url = self.shop_url + '/webservice/webservice.asmx/SearchWebService?'\ 'searchText=%s&searchContext=ebook' % urllib2.quote(query) + search_urls = [ search_url ] + + ## add this as the fist try if it looks like ozon ID + if re.match("^\d{6,9}$", query): + ozon_detail = self.shop_url + '/webservices/OzonWebSvc.asmx/ItemDetail?ID=%s' % query + search_urls.insert(0, ozon_detail) + xp_template = 'normalize-space(./*[local-name() = "{0}"]/text())' - counter = max_results br = browser() - with closing(br.open(search_url, timeout=timeout)) as f: - raw = xml_to_unicode(f.read(), strip_encoding_pats=True, assume_utf8=True)[0] - doc = etree.fromstring(raw) - for data in doc.xpath('//*[local-name() = "SearchItems"]'): - if counter <= 0: - break - counter -= 1 + + for url in search_urls: + with closing(br.open(url, timeout=timeout)) as f: + raw = xml_to_unicode(f.read(), strip_encoding_pats=True, assume_utf8=True)[0] + doc = etree.fromstring(raw) + for data in doc.xpath('//*[local-name()="SearchItems" or local-name()="ItemDetail"]'): + if counter <= 0: + break + counter -= 1 - s = SearchResult() - s.detail_item = data.xpath(xp_template.format('ID')) - s.title = data.xpath(xp_template.format('Name')) - s.author = data.xpath(xp_template.format('Author')) - s.price = data.xpath(xp_template.format('Price')) - s.cover_url = data.xpath(xp_template.format('Picture')) - s.price = format_price_in_RUR(s.price) - yield s + s = SearchResult() + s.detail_item = data.xpath(xp_template.format('ID')) + s.title = data.xpath(xp_template.format('Name')) + s.author = data.xpath(xp_template.format('Author')) + s.price = data.xpath(xp_template.format('Price')) + s.cover_url = data.xpath(xp_template.format('Picture')) + s.price = format_price_in_RUR(s.price) + yield s def get_details(self, search_result, timeout=60): url = self.shop_url + '/context/detail/id/' + urllib2.quote(search_result.detail_item) @@ -97,6 +104,16 @@ class OzonRUStore(BasicStoreConfig, StorePlugin): search_result.formats = ', '.join(_parse_ebook_formats(formats)) # unfortunately no direct links to download books (only buy link) # search_result.downloads['BF2'] = self.shop_url + '/order/digitalorder.aspx?id=' + + urllib2.quote(search_result.detail_item) + + #

21500 руб.

+ # + # + + # if the price not in the search result (the ID search case) + if not search_result.price: + price = doc.xpath(u'normalize-space(//*[@itemprop="price"]/text())') + search_result.price = format_price_in_RUR(price) + return result def format_price_in_RUR(price): diff --git a/src/calibre/gui2/store/stores/weightless_books_plugin.py b/src/calibre/gui2/store/stores/weightless_books_plugin.py index 3fa1c76851..330f3fdf0f 100644 --- a/src/calibre/gui2/store/stores/weightless_books_plugin.py +++ b/src/calibre/gui2/store/stores/weightless_books_plugin.py @@ -41,7 +41,7 @@ class WeightlessBooksStore(BasicStoreConfig, StorePlugin): counter = max_results with closing(br.open(url, timeout=timeout)) as f: doc = html.fromstring(f.read()) - for data in doc.xpath('//li[@id="product"]'): + for data in doc.xpath('//li[@class="product"]'): if counter <= 0: break diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index ed36bf125c..10d10b7155 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -234,21 +234,27 @@ class Document(QWebPage): # {{{ def switch_to_fullscreen_mode(self): self.in_fullscreen_mode = True - self.javascript(''' - var s = document.body.style; - s.maxWidth = "%dpx"; - s.marginLeft = "auto"; - s.marginRight = "auto"; - '''%self.max_fs_width) + if self.in_paged_mode: + self.javascript('paged_display.max_col_width = %d'%self.max_fs_width) + else: + self.javascript(''' + var s = document.body.style; + s.maxWidth = "%dpx"; + s.marginLeft = "auto"; + s.marginRight = "auto"; + '''%self.max_fs_width) def switch_to_window_mode(self): self.in_fullscreen_mode = False - self.javascript(''' - var s = document.body.style; - s.maxWidth = "none"; - s.marginLeft = "%s"; - s.marginRight = "%s"; - '''%(self.initial_left_margin, self.initial_right_margin)) + if self.in_paged_mode: + self.javascript('paged_display.max_col_width = %d'%-1) + else: + self.javascript(''' + var s = document.body.style; + s.maxWidth = "none"; + s.marginLeft = "%s"; + s.marginRight = "%s"; + '''%(self.initial_left_margin, self.initial_right_margin)) @pyqtSignature("QString") def debug(self, msg): diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 08ab731e51..4a39a6ae8d 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -477,6 +477,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): else: self.view.document.switch_to_window_mode() self.view.document.page_position.restore() + self.scrolled(self.view.scroll_fraction) def goto(self, ref): if ref: @@ -754,12 +755,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer): # There hasn't been a resize event for some time # restore the current page position. self.resize_in_progress = False - self.view.document.after_resize() if self.window_mode_changed: # This resize is part of a window mode change, special case it self.handle_window_mode_toggle() else: self.view.document.page_position.restore() + self.view.document.after_resize() def close_progress_indicator(self): self.pi.stop() diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 3a798a961b..6c24a1e455 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -829,7 +829,9 @@ def parse_series_string(db, label, value): val = pat.sub('', val).strip() s_index = float(match.group(1)) elif val: - if tweaks['series_index_auto_increment'] != 'const': + if tweaks['series_index_auto_increment'] == 'no_change': + pass + elif tweaks['series_index_auto_increment'] != 'const': s_index = db.get_next_cc_series_num_for(val, label=label) else: s_index = 1.0 diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 18895253d0..32cb6737a8 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1203,7 +1203,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if m: return m['mtime'] - def format_metadata(self, id_, fmt, allow_cache=True): + def format_metadata(self, id_, fmt, allow_cache=True, update_db=False, + commit=False): if not fmt: return {} fmt = fmt.upper() @@ -1218,6 +1219,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ans['size'] = stat.st_size ans['mtime'] = utcfromtimestamp(stat.st_mtime) self.format_metadata_cache[id_][fmt] = ans + if update_db: + self.conn.execute( + 'UPDATE data SET uncompressed_size=? WHERE format=? AND' + ' book=?', (stat.st_size, fmt, id_)) + if commit: + self.conn.commit() return ans def format_hash(self, id_, fmt): @@ -1448,6 +1455,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', [id]) + def clean_standard_field(self, field, commit=False): + # Don't bother with validity checking. Let the exception fly out so + # we can see what happened + def doit(table, ltable_col): + st = ('DELETE FROM books_%s_link WHERE (SELECT COUNT(id) ' + 'FROM books WHERE id=book) < 1;')%table + self.conn.execute(st) + st = ('DELETE FROM %(table)s WHERE (SELECT COUNT(id) ' + 'FROM books_%(table)s_link WHERE ' + '%(ltable_col)s=%(table)s.id) < 1;') % dict( + table=table, ltable_col=ltable_col) + self.conn.execute(st) + + fm = self.field_metadata[field] + doit(fm['table'], fm['link_column']) + if commit: + self.conn.commit() + def clean(self): ''' Remove orphaned entries. @@ -2550,6 +2575,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.set_tags(book_id, new_names, append=True, notify=False, commit=False) self.dirtied(books, commit=False) + self.clean_standard_field('tags', commit=False) self.conn.commit() def delete_tag_using_id(self, id): @@ -2564,7 +2590,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return [] return result - def rename_series(self, old_id, new_name): + def rename_series(self, old_id, new_name, change_index=True): new_name = new_name.strip() new_id = self.conn.get( '''SELECT id from series @@ -2577,23 +2603,26 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # New series exists. Must update the link, then assign a # new series index to each of the books. - # Get the list of books where we must update the series index - books = self.conn.get('''SELECT books.id - FROM books, books_series_link as lt - WHERE books.id = lt.book AND lt.series=? - ORDER BY books.series_index''', (old_id,)) + if change_index: + # Get the list of books where we must update the series index + books = self.conn.get('''SELECT books.id + FROM books, books_series_link as lt + WHERE books.id = lt.book AND lt.series=? + ORDER BY books.series_index''', (old_id,)) # Now update the link table self.conn.execute('''UPDATE books_series_link SET series=? WHERE series=?''',(new_id, old_id,)) - # Now set the indices - for (book_id,) in books: - # Get the next series index - index = self.get_next_series_num_for(new_name) - self.conn.execute('''UPDATE books - SET series_index=? - WHERE id=?''',(index, book_id,)) + if change_index and tweaks['series_index_auto_increment'] != 'no_change': + # Now set the indices + for (book_id,) in books: + # Get the next series index + index = self.get_next_series_num_for(new_name) + self.conn.execute('''UPDATE books + SET series_index=? + WHERE id=?''',(index, book_id,)) self.dirty_books_referencing('series', new_id, commit=False) + self.clean_standard_field('series', commit=False) self.conn.commit() def delete_series_using_id(self, id): @@ -2629,6 +2658,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Get rid of the no-longer used publisher self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,)) self.dirty_books_referencing('publisher', new_id, commit=False) + self.clean_standard_field('publisher', commit=False) self.conn.commit() def delete_publisher_using_id(self, old_id): @@ -2727,7 +2757,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # metadata. Ignore it. pass # Now delete the old author from the DB - bks = self.conn.get('SELECT book FROM books_authors_link WHERE author=?', (old_id,)) self.conn.execute('DELETE FROM authors WHERE id=?', (old_id,)) self.dirtied(books, commit=False) self.conn.commit() @@ -3684,4 +3713,12 @@ 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_usage_count_by_id(self, field): + fm = self.field_metadata[field] + if not fm.get('link_column', None): + raise ValueError('%s is not an is_multiple field') + return self.conn.get( + 'SELECT {0}, count(*) FROM books_{1}_link GROUP BY {0}'.format( + fm['link_column'], fm['table'])) +