diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index bf83a4c60b..336d015e44 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -62,6 +62,18 @@ div.description { text-indent: 1em; } +/* +* Attempt to minimize widows and orphans by logically grouping chunks +* Recommend enabling for iPad +* Some reports of problems with Sony ereaders, presumably ADE engines +*/ +/* +div.logical_group { + display:inline-block; + width:100%; + } +*/ + p.date_index { font-size:x-large; text-align:center; diff --git a/resources/recipes/20_minutos.recipe b/resources/recipes/20_minutos.recipe index 8205c918f5..1f862847dc 100644 --- a/resources/recipes/20_minutos.recipe +++ b/resources/recipes/20_minutos.recipe @@ -1,17 +1,67 @@ +# -*- coding: utf-8 +__license__ = 'GPL v3' +__author__ = 'Luis Hernandez' +__copyright__ = 'Luis Hernandez' +description = 'Periódico gratuito en español - v0.5 - 25 Jan 2011' + +''' +www.20minutos.es +''' + from calibre.web.feeds.news import BasicNewsRecipe -class AdvancedUserRecipe1295310874(BasicNewsRecipe): - title = u'20 Minutos (Boletin)' - __author__ = 'Luis Hernandez' - description = 'Periódico gratuito en español' +class AdvancedUserRecipe1294946868(BasicNewsRecipe): + + title = u'20 Minutos' + publisher = u'Grupo 20 Minutos' + + __author__ = u'Luis Hernández' + description = u'Periódico gratuito en español' cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif' - language = 'es' - oldest_article = 2 - max_articles_per_feed = 50 + oldest_article = 5 + max_articles_per_feed = 100 + + remove_javascript = True + no_stylesheets = True + use_embedded_content = False + + encoding = 'ISO-8859-1' + language = 'es' + timefmt = '[%a, %d %b, %Y]' + + keep_only_tags = [dict(name='div', attrs={'id':['content']}) + ,dict(name='div', attrs={'class':['boxed','description','lead','article-content']}) + ,dict(name='span', attrs={'class':['photo-bar']}) + ,dict(name='ul', attrs={'class':['article-author']}) + ] + + remove_tags_before = dict(name='ul' , attrs={'class':['servicios-sub']}) + remove_tags_after = dict(name='div' , attrs={'class':['related-news','col']}) + + remove_tags = [ + dict(name='ol', attrs={'class':['navigation',]}) + ,dict(name='span', attrs={'class':['action']}) + ,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col']}) + ,dict(name='div', attrs={'id':['twitter-destacados']}) + ,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']}) + ] + + feeds = [ + (u'Portada' , u'http://www.20minutos.es/rss/') + ,(u'Nacional' , u'http://www.20minutos.es/rss/nacional/') + ,(u'Internacional' , u'http://www.20minutos.es/rss/internacional/') + ,(u'Economia' , u'http://www.20minutos.es/rss/economia/') + ,(u'Deportes' , u'http://www.20minutos.es/rss/deportes/') + ,(u'Tecnologia' , u'http://www.20minutos.es/rss/tecnologia/') + ,(u'Gente - TV' , u'http://www.20minutos.es/rss/gente-television/') + ,(u'Motor' , u'http://www.20minutos.es/rss/motor/') + ,(u'Salud' , u'http://www.20minutos.es/rss/belleza-y-salud/') + ,(u'Viajes' , u'http://www.20minutos.es/rss/viajes/') + ,(u'Vivienda' , u'http://www.20minutos.es/rss/vivienda/') + ,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/') + ,(u'Cine' , u'http://www.20minutos.es/rss/cine/') + ,(u'Musica' , u'http://www.20minutos.es/rss/musica/') + ,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/') + ] - feeds = [(u'VESPERTINO', u'http://20minutos.feedsportal.com/c/32489/f/478284/index.rss') - , (u'DEPORTES', u'http://20minutos.feedsportal.com/c/32489/f/478286/index.rss') - , (u'CULTURA', u'http://www.20minutos.es/rss/ocio/') - , (u'TV', u'http://20minutos.feedsportal.com/c/32489/f/490877/index.rss') -] diff --git a/resources/recipes/new_yorker.recipe b/resources/recipes/new_yorker.recipe index 0c95aa358d..d69a4df24f 100644 --- a/resources/recipes/new_yorker.recipe +++ b/resources/recipes/new_yorker.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2008-2010, Darko Miletic ' +__copyright__ = '2008-2011, Darko Miletic ' ''' newyorker.com ''' @@ -54,10 +54,10 @@ class NewYorker(BasicNewsRecipe): ,dict(attrs={'id':['show-header','show-footer'] }) ] remove_attributes = ['lang'] - feeds = [(u'The New Yorker', u'http://feeds.newyorker.com/services/rss/feeds/everything.xml')] + feeds = [(u'The New Yorker', u'http://www.newyorker.com/services/rss/feeds/everything.xml')] def print_version(self, url): - return url + '?printable=true' + return 'http://www.newyorker.com' + url + '?printable=true' def image_url_processor(self, baseurl, url): return url.strip() diff --git a/resources/recipes/nytimes_sub.recipe b/resources/recipes/nytimes_sub.recipe index 2424113e31..81b8bd5cb7 100644 --- a/resources/recipes/nytimes_sub.recipe +++ b/resources/recipes/nytimes_sub.recipe @@ -1,6 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' @@ -28,6 +27,10 @@ class NYTimes(BasicNewsRecipe): # previous paid versions of the new york times to best sent to the back issues folder on the kindle replaceKindleVersion = False + # download higher resolution images than the small thumbnails typically included in the article + # the down side of having large beautiful images is the file size is much larger, on the order of 7MB per paper + useHighResImages = True + # includeSections: List of sections to include. If empty, all sections found will be included. # Otherwise, only the sections named will be included. For example, # @@ -90,7 +93,6 @@ class NYTimes(BasicNewsRecipe): (u'Sunday Magazine',u'magazine'), (u'Week in Review',u'weekinreview')] - if headlinesOnly: title='New York Times Headlines' description = 'Headlines from the New York Times' @@ -127,7 +129,7 @@ class NYTimes(BasicNewsRecipe): earliest_date = date.today() - timedelta(days=oldest_article) - __author__ = 'GRiker/Kovid Goyal/Nick Redding' + __author__ = 'GRiker/Kovid Goyal/Nick Redding/Ben Collier' language = 'en' requires_version = (0, 7, 5) @@ -149,7 +151,7 @@ class NYTimes(BasicNewsRecipe): 'dottedLine', 'entry-meta', 'entry-response module', - 'icon enlargeThis', + #'icon enlargeThis', #removed to provide option for high res images 'leftNavTabs', 'metaFootnote', 'module box nav', @@ -163,7 +165,23 @@ class NYTimes(BasicNewsRecipe): 'entry-tags', #added for DealBook 'footer promos clearfix', #added for DealBook 'footer links clearfix', #added for DealBook - 'inlineImage module', #added for DealBook + 'tabsContainer', #added for other blog downloads + 'column lastColumn', #added for other blog downloads + 'pageHeaderWithLabel', #added for other gadgetwise downloads + 'column two', #added for other blog downloads + 'column two last', #added for other blog downloads + 'column three', #added for other blog downloads + 'column three last', #added for other blog downloads + 'column four',#added for other blog downloads + 'column four last',#added for other blog downloads + 'column last', #added for other blog downloads + 'timestamp published', #added for other blog downloads + 'entry entry-related', + 'subNavigation tabContent active', #caucus blog navigation + 'columnGroup doubleRule', + 'mediaOverlay slideshow', + 'headlinesOnly multiline flush', + 'wideThumb', re.compile('^subNavigation'), re.compile('^leaderboard'), re.compile('^module'), @@ -254,7 +272,7 @@ class NYTimes(BasicNewsRecipe): def exclude_url(self,url): if not url.startswith("http"): return True - if not url.endswith(".html") and 'dealbook.nytimes.com' not in url: #added for DealBook + if not url.endswith(".html") and 'dealbook.nytimes.com' not in url and 'blogs.nytimes.com' not in url: #added for DealBook return True if 'nytimes.com' not in url: return True @@ -592,19 +610,84 @@ class NYTimes(BasicNewsRecipe): self.log("Skipping article dated %s" % date_str) return None - kicker_tag = soup.find(attrs={'class':'kicker'}) - if kicker_tag: # remove Op_Ed author head shots - tagline = self.tag_to_string(kicker_tag) - if tagline=='Op-Ed Columnist': - img_div = soup.find('div','inlineImage module') - if img_div: - img_div.extract() + #all articles are from today, no need to print the date on every page + try: + if not self.webEdition: + date_tag = soup.find(True,attrs={'class': ['dateline','date']}) + if date_tag: + date_tag.extract() + except: + self.log("Error removing the published date") + if self.useHighResImages: + try: + #open up all the "Enlarge this Image" pop-ups and download the full resolution jpegs + enlargeThisList = soup.findAll('div',{'class':'icon enlargeThis'}) + if enlargeThisList: + for popupref in enlargeThisList: + popupreflink = popupref.find('a') + if popupreflink: + reflinkstring = str(popupreflink['href']) + refstart = reflinkstring.find("javascript:pop_me_up2('") + len("javascript:pop_me_up2('") + refend = reflinkstring.find(".html", refstart) + len(".html") + reflinkstring = reflinkstring[refstart:refend] + + popuppage = self.browser.open(reflinkstring) + popuphtml = popuppage.read() + popuppage.close() + if popuphtml: + st = time.localtime() + year = str(st.tm_year) + month = "%.2d" % st.tm_mon + day = "%.2d" % st.tm_mday + imgstartpos = popuphtml.find('http://graphics8.nytimes.com/images/' + year + '/' + month +'/' + day +'/') + len('http://graphics8.nytimes.com/images/' + year + '/' + month +'/' + day +'/') + highResImageLink = 'http://graphics8.nytimes.com/images/' + year + '/' + month +'/' + day +'/' + popuphtml[imgstartpos:popuphtml.find('.jpg',imgstartpos)+4] + popupSoup = BeautifulSoup(popuphtml) + highResTag = popupSoup.find('img', {'src':highResImageLink}) + if highResTag: + try: + newWidth = highResTag['width'] + newHeight = highResTag['height'] + imageTag = popupref.parent.find("img") + except: + self.log("Error: finding width and height of img") + popupref.extract() + if imageTag: + try: + imageTag['src'] = highResImageLink + imageTag['width'] = newWidth + imageTag['height'] = newHeight + except: + self.log("Error setting the src width and height parameters") + except Exception: + self.log("Error pulling high resolution images") + + try: + #remove "Related content" bar + runAroundsFound = soup.findAll('div',{'class':['articleInline runaroundLeft','articleInline doubleRule runaroundLeft','articleInline runaroundLeft firstArticleInline']}) + if runAroundsFound: + for runAround in runAroundsFound: + #find all section headers + hlines = runAround.findAll(True ,{'class':['sectionHeader','sectionHeader flushBottom']}) + if hlines: + for hline in hlines: + hline.extract() + except: + self.log("Error removing related content bar") + + + try: + #in case pulling images failed, delete the enlarge this text + enlargeThisList = soup.findAll('div',{'class':'icon enlargeThis'}) + if enlargeThisList: + for popupref in enlargeThisList: + popupref.extract() + except: + self.log("Error removing Enlarge this text") return self.strip_anchors(soup) def postprocess_html(self,soup, True): - try: if self.one_picture_per_article: # Remove all images after first @@ -766,6 +849,8 @@ class NYTimes(BasicNewsRecipe): try: if len(article.text_summary.strip()) == 0: articlebodies = soup.findAll('div',attrs={'class':'articleBody'}) + if not articlebodies: #added to account for blog formats + articlebodies = soup.findAll('div', attrs={'class':'entry-content'}) #added to account for blog formats if articlebodies: for articlebody in articlebodies: if articlebody: @@ -774,13 +859,14 @@ class NYTimes(BasicNewsRecipe): refparagraph = self.massageNCXText(self.tag_to_string(p,use_alt=False)).strip() #account for blank paragraphs and short paragraphs by appending them to longer ones if len(refparagraph) > 0: - if len(refparagraph) > 70: #approximately one line of text + if len(refparagraph) > 140: #approximately two lines of text article.summary = article.text_summary = shortparagraph + refparagraph return else: shortparagraph = refparagraph + " " if shortparagraph.strip().find(" ") == -1 and not shortparagraph.strip().endswith(":"): shortparagraph = shortparagraph + "- " + except: self.log("Error creating article descriptions") return diff --git a/resources/recipes/sinfest.recipe b/resources/recipes/sinfest.recipe new file mode 100644 index 0000000000..bb0ef2e22e --- /dev/null +++ b/resources/recipes/sinfest.recipe @@ -0,0 +1,33 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Nadid ' +''' +http://www.sinfest.net +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class SinfestBig(BasicNewsRecipe): + title = 'Sinfest' + __author__ = 'nadid' + description = 'Sinfest' + reverse_article_order = False + oldest_article = 5 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = True + encoding = 'utf-8' + publisher = 'Tatsuya Ishida/Museworks' + category = 'comic' + language = 'en' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + feeds = [(u'SinFest', u'http://henrik.nyh.se/scrapers/sinfest.rss' )] + def get_article_url(self, article): + return article.get('link') + diff --git a/src/calibre/devices/folder_device/driver.py b/src/calibre/devices/folder_device/driver.py index b852715b97..d75697a6cb 100644 --- a/src/calibre/devices/folder_device/driver.py +++ b/src/calibre/devices/folder_device/driver.py @@ -22,7 +22,7 @@ class FOLDER_DEVICE_FOR_CONFIG(USBMS): PRODUCT_ID = [0xffff] BCD = [0xffff] DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' - + SUPPORTS_SUB_DIRS = True class FOLDER_DEVICE(USBMS): type = _('Device Interface') diff --git a/src/calibre/ebooks/metadata/sources/__init__.py b/src/calibre/ebooks/metadata/sources/__init__.py new file mode 100644 index 0000000000..68dfb8d2b5 --- /dev/null +++ b/src/calibre/ebooks/metadata/sources/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + + diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 8e11ac6498..d08a68c0bc 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -17,7 +17,7 @@ from lxml import etree import cssutils from calibre.ebooks.oeb.base import OPF1_NS, OPF2_NS, OPF2_NSMAP, DC11_NS, \ - DC_NSES, OPF + DC_NSES, OPF, xml2text from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, OEB_IMAGES, \ PAGE_MAP_MIME, JPEG_MIME, NCX_MIME, SVG_MIME from calibre.ebooks.oeb.base import XMLDECL_RE, COLLAPSE_RE, \ @@ -423,7 +423,7 @@ class OEBReader(object): path, frag = urldefrag(href) if path not in self.oeb.manifest.hrefs: continue - title = ' '.join(xpath(anchor, './/text()')) + title = xml2text(anchor) title = COLLAPSE_RE.sub(' ', title.strip()) if href not in titles: order.append(href) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index c94b99f141..84a26cea18 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -550,6 +550,14 @@ def choose_dir(window, name, title, default_dir='~'): if dir: return dir[0] +def choose_osx_app(window, name, title, default_dir='/Applications'): + fd = FileDialog(title=title, parent=window, name=name, mode=QFileDialog.ExistingFile, + default_dir=default_dir) + app = fd.get_files() + fd.setParent(None) + if app: + return app + def choose_files(window, name, title, filters=[], all_files=True, select_only_single_file=False): ''' diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index dfafcd1a39..a702ba045e 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -9,7 +9,7 @@ import os, datetime from PyQt4.Qt import pyqtSignal, QModelIndex, QThread, Qt -from calibre.gui2 import error_dialog, gprefs +from calibre.gui2 import error_dialog from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString from calibre import strftime from calibre.gui2.actions import InterfaceAction @@ -165,10 +165,12 @@ class FetchAnnotationsAction(InterfaceAction): ka_soup.insert(0,divTag) return ka_soup + ''' def mark_book_as_read(self,id): read_tag = gprefs.get('catalog_epub_mobi_read_tag') if read_tag: self.db.set_tags(id, [read_tag], append=True) + ''' def canceled(self): self.pd.hide() @@ -201,10 +203,12 @@ class FetchAnnotationsAction(InterfaceAction): # Update library comments self.db.set_comment(id, mi.comments) + ''' # Update 'read' tag except for Catalogs/Clippings if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD: if not set(mi.tags).intersection(ignore_tags): self.mark_book_as_read(id) + ''' # Add bookmark file to id self.db.add_format_with_hooks(id, bm.value.bookmark_extension, diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index d726241432..001970f9db 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -390,7 +390,7 @@ class ChooseLibraryAction(InterfaceAction): #self.dbref = weakref.ref(self.gui.library_view.model().db) #self.before_mem = memory()/1024**2 self.gui.library_moved(loc) - #QTimer.singleShot(1000, self.debug_leak) + #QTimer.singleShot(5000, self.debug_leak) def debug_leak(self): import gc @@ -398,7 +398,7 @@ class ChooseLibraryAction(InterfaceAction): ref = self.dbref for i in xrange(3): gc.collect() if ref() is not None: - print 11111, ref() + print 'DB object alive:', ref() for r in gc.get_referrers(ref())[:10]: print r print diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 94760306c3..d5149569be 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -335,7 +335,7 @@ class PluginWidget(QWidget,Ui_Form): ''' return - + ''' if new_state == 0: # unchecked self.merge_source_field.setEnabled(False) @@ -348,6 +348,7 @@ class PluginWidget(QWidget,Ui_Form): self.merge_before.setEnabled(True) self.merge_after.setEnabled(True) self.include_hr.setEnabled(True) + ''' def header_note_source_field_changed(self,new_index): ''' diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index ec59268ec8..88446344ec 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -42,9 +42,15 @@ class SearchAndReplaceWidget(Widget, Ui_Form): def break_cycles(self): Widget.break_cycles(self) - self.opt_sr1_search.doc_update.disconnect() - self.opt_sr2_search.doc_update.disconnect() - self.opt_sr3_search.doc_update.disconnect() + def d(x): + try: + x.disconnect() + except: + pass + + d(self.opt_sr1_search) + d(self.opt_sr2_search) + d(self.opt_sr3_search) self.opt_sr1_search.break_cycles() self.opt_sr2_search.break_cycles() diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 572bbcf1c4..8aa624cacc 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -250,22 +250,27 @@ class Scheduler(QObject): self.timer = QTimer(self) self.timer.start(int(self.INTERVAL * 60 * 1000)) - self.oldest_timer = QTimer() - self.connect(self.oldest_timer, SIGNAL('timeout()'), self.oldest_check) self.connect(self.timer, SIGNAL('timeout()'), self.check) self.oldest = gconf['oldest_news'] - self.oldest_timer.start(int(60 * 60 * 1000)) QTimer.singleShot(5 * 1000, self.oldest_check) self.database_changed = self.recipe_model.database_changed def oldest_check(self): if self.oldest > 0: delta = timedelta(days=self.oldest) - ids = self.recipe_model.db.tags_older_than(_('News'), delta) + try: + ids = self.recipe_model.db.tags_older_than(_('News'), delta) + except: + # Should never happen + ids = [] + import traceback + traceback.print_exc() if ids: ids = list(ids) if ids: self.delete_old_news.emit(ids) + QTimer.singleShot(60 * 60 * 1000, self.oldest_check) + def show_dialog(self, *args): self.lock.lock() diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 8b574948ff..2160e13b65 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -98,6 +98,7 @@ class TagsView(QTreeView): # {{{ self.collapse_model = 'disable' else: self.collapse_model = gprefs['tags_browser_partition_method'] + self.search_icon = QIcon(I('search.png')) def set_pane_is_visible(self, to_what): pv = self.pane_is_visible @@ -199,6 +200,10 @@ class TagsView(QTreeView): # {{{ if action == 'manage_categories': self.user_category_edit.emit(category) return + if action == 'search': + self.tags_marked.emit(('not ' if negate else '') + + category + ':"=' + key + '"') + return if action == 'search_category': self.tags_marked.emit(category + ':' + str(not negate)) return @@ -208,6 +213,7 @@ class TagsView(QTreeView): # {{{ if action == 'edit_author_sort': self.author_sort_edit.emit(self, index) return + if action == 'hide': self.hidden_categories.add(category) elif action == 'show': @@ -248,19 +254,36 @@ class TagsView(QTreeView): # {{{ if key not in self.db.field_metadata: return True - # If the user right-clicked on an editable item, then offer - # the possibility of renaming that item - if tag_name and \ - (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ - self.db.field_metadata[key]['is_custom'] and \ - self.db.field_metadata[key]['datatype'] != 'rating'): - self.context_menu.addAction(_('Rename \'%s\'')%tag_name, - partial(self.context_menu_handler, action='edit_item', - category=tag_item, index=index)) - if key == 'authors': - self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name, - partial(self.context_menu_handler, - action='edit_author_sort', index=tag_id)) + # Did the user click on a leaf node? + if tag_name: + # If the user right-clicked on an editable item, then offer + # the possibility of renaming that item. + if key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ + (self.db.field_metadata[key]['is_custom'] and \ + self.db.field_metadata[key]['datatype'] != 'rating'): + # Add the 'rename' items + self.context_menu.addAction(_('Rename %s')%tag_name, + partial(self.context_menu_handler, action='edit_item', + category=tag_item, index=index)) + if key == 'authors': + self.context_menu.addAction(_('Edit sort for %s')%tag_name, + partial(self.context_menu_handler, + action='edit_author_sort', index=tag_id)) + # Add the search for value items + n = tag_name + c = category + if self.db.field_metadata[key]['datatype'] == 'rating': + n = str(len(tag_name)) + elif self.db.field_metadata[key]['kind'] in ['user', 'search']: + c = tag_item.tag.category + self.context_menu.addAction(self.search_icon, + _('Search for %s')%tag_name, + partial(self.context_menu_handler, action='search', + category=c, key=n, negate=False)) + self.context_menu.addAction(self.search_icon, + _('Search for everything but %s')%tag_name, + partial(self.context_menu_handler, action='search', + category=c, key=n, negate=True)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction(_('Hide category %s') % category, @@ -272,14 +295,15 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='show', category=col)) # search by category - self.context_menu.addAction( - _('Search for books in category %s')%category, - partial(self.context_menu_handler, action='search_category', - category=key, negate=False)) - self.context_menu.addAction( - _('Search for books not in category %s')%category, - partial(self.context_menu_handler, action='search_category', - category=key, negate=True)) + if key != 'search': + self.context_menu.addAction(self.search_icon, + _('Search for books in category %s')%category, + partial(self.context_menu_handler, action='search_category', + category=key, negate=False)) + self.context_menu.addAction(self.search_icon, + _('Search for books not in category %s')%category, + partial(self.context_menu_handler, action='search_category', + category=key, negate=True)) # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() if key in ['tags', 'publisher', 'series'] or \ @@ -703,7 +727,11 @@ class TagsModel(QAbstractItemModel): # {{{ for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(), key=sort_key): cat_name = '@' + user_cat # add the '@' to avoid name collision - tb_cats.add_user_category(label=cat_name, name=user_cat) + try: + tb_cats.add_user_category(label=cat_name, name=user_cat) + except ValueError: + import traceback + traceback.print_exc() if len(saved_searches().names()): tb_cats.add_search_category(label='search', name=_('Searches')) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 6a74ccd6ea..c0658536bb 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -638,8 +638,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ except KeyboardInterrupt: pass time.sleep(2) - if mb is not None: - mb.flush() self.hide_windows() return True diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 77e75736cf..af47a79e49 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -42,6 +42,8 @@ class MetadataBackup(Thread): # {{{ def stop(self): self.keep_running = False + + def break_cycles(self): # Break cycles so that this object doesn't hold references to db self.do_write = self.get_metadata_for_dump = self.clear_dirtied = \ self.set_dirtied = self.db = None @@ -57,7 +59,10 @@ class MetadataBackup(Thread): # {{{ except: # Happens during interpreter shutdown break + if not self.keep_running: + break + self.in_limbo = id_ try: path, mi = self.get_metadata_for_dump(id_) except: @@ -72,10 +77,10 @@ class MetadataBackup(Thread): # {{{ continue # at this point the dirty indication is off - if mi is None: continue - self.in_limbo = id_ + if not self.keep_running: + break # Give the GUI thread a chance to do something. Python threads don't # have priorities, so this thread would naturally keep the processor @@ -89,6 +94,9 @@ class MetadataBackup(Thread): # {{{ traceback.print_exc() continue + if not self.keep_running: + break + time.sleep(0.1) # Give the GUI thread a chance to do something try: self.do_write(path, raw) @@ -102,7 +110,10 @@ class MetadataBackup(Thread): # {{{ prints('Failed to write backup metadata for id:', id_, 'again, giving up') continue - self.in_limbo = None + + self.in_limbo = None + self.flush() + self.break_cycles() def flush(self): 'Used during shutdown to ensure that a dirtied book is not missed' @@ -111,6 +122,7 @@ class MetadataBackup(Thread): # {{{ self.db.dirtied([self.in_limbo]) except: traceback.print_exc() + self.in_limbo = None def write(self, path, raw): with lopen(path, 'wb') as f: diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 95e738dd58..f0e4778de4 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1820,6 +1820,9 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) self.booksByTitle_noSeriesPrefix = nspt # Loop through the books by title + # Generate one divRunningTag per initial letter for the purposes of + # minimizing widows and orphans on readers that can handle large + # styled as inline-block title_list = self.booksByTitle if not self.useSeriesPrefixInTitlesSection: title_list = self.booksByTitle_noSeriesPrefix @@ -1832,7 +1835,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) divTag.insert(dtc, divRunningTag) dtc += 1 divRunningTag = Tag(soup, 'div') - divRunningTag['style'] = 'display:inline-block;width:100%' + divRunningTag['class'] = "logical_group" drtc = 0 current_letter = self.letter_or_symbol(book['title_sort'][0]) pIndexTag = Tag(soup, "p") @@ -1954,6 +1957,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) drtc = 0 # Loop through booksByAuthor + # Each author/books group goes in an openingTag div (first) or + # a runningTag div (subsequent) book_count = 0 current_author = '' current_letter = '' @@ -1977,7 +1982,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) current_letter = self.letter_or_symbol(book['author_sort'][0].upper()) author_count = 0 divOpeningTag = Tag(soup, 'div') - divOpeningTag['style'] = 'display:inline-block;width:100%' + divOpeningTag['class'] = "logical_group" dotc = 0 pIndexTag = Tag(soup, "p") pIndexTag['class'] = "letter_index" @@ -2001,7 +2006,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Create a divRunningTag for the rest of the authors in this letter divRunningTag = Tag(soup, 'div') - divRunningTag['style'] = 'display:inline-block;width:100%' + divRunningTag['class'] = "logical_group" drtc = 0 non_series_books = 0 diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f2b2c94e31..ed47abbdb3 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1376,10 +1376,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def tags_older_than(self, tag, delta): tag = tag.lower().strip() now = nowf() + tindex = self.FIELD_MAP['timestamp'] + gindex = self.FIELD_MAP['tags'] for r in self.data._data: if r is not None: - if (now - r[self.FIELD_MAP['timestamp']]) > delta: - tags = r[self.FIELD_MAP['tags']] + if (now - r[tindex]) > delta: + tags = r[gindex] if tags and tag in [x.strip() for x in tags.lower().split(',')]: yield r[self.FIELD_MAP['id']] diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 78fe899fa8..93ac607fcf 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -477,6 +477,8 @@ class FieldMetadata(dict): del self._tb_cats[key] if key in self._search_term_map: del self._search_term_map[key] + if key.lower() in self._search_term_map: + del self._search_term_map[key.lower()] def cc_series_index_column_for(self, key): return self._tb_cats[key]['rec_index'] + 1 diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 5ebe91bc76..7a04e0f642 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -310,7 +310,9 @@ What formats does |app| read metadata from? Where are the book files stored? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Metadata about the books is stored in the file ``metadata.db`` (which is a sqlite database). +When you first run |app|, it will ask you for a folder in which to store your books. Whenever you add a book to |app|, it will copy the book into that folder. Books in the folder are nicely arranged into sub-folders by Author and Title. Note that the contents of this folder are automatically managed by |app|, **do not** add any files/folders manually to this folder, as they may be automatically deleted. If you want to add a file associated to a particular book, use the top right area of :guilabel:`Edit metadata` dialog to do so. Then, |app| will automatically put that file into the correct folder and move it around when the title/author changes. + +Metadata about the books is stored in the file ``metadata.db`` at the top level of the library folder This file is is a sqlite database. When backing up your library make sure you copy the entire folder and all its sub-folders. Why doesn't |app| let me store books in my own directory structure? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~