Sync to trunk.

This commit is contained in:
John Schember 2011-02-15 18:16:46 -05:00
commit cc3c0468a2
27 changed files with 1616 additions and 612 deletions

View File

@ -245,19 +245,6 @@ sony_collection_name_template='{value}{category:| (|)}'
sony_collection_sorting_rules = [] sony_collection_sorting_rules = []
#: Create search terms to apply a query across several built-in search terms.
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
# Example: create the term 'myseries' that when used as myseries:foo would
# search all of the search categories 'series', '#myseries', and '#myseries2':
# grouped_search_terms={'myseries':['series','#myseries', '#myseries2']}
# Example: two search terms 'a' and 'b' both that search 'tags' and '#mytags':
# grouped_search_terms={'a':['tags','#mytags'], 'b':['tags','#mytags']}
# Note: You cannot create a search term that is a duplicate of an existing term.
# Such duplicates will be silently ignored. Also note that search terms ignore
# case. 'MySearch' and 'mysearch' are the same term.
grouped_search_terms = {}
#: Control how tags are applied when copying books to another library #: Control how tags are applied when copying books to another library
# Set this to True to ensure that tags in 'Tags to add when adding # Set this to True to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library # a book' are added when copying books to another library

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
adevarul.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Adevarul(BasicNewsRecipe):
title = u'Adev\u0103rul'
language = 'ro'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
publisher = 'Adevarul'
category = 'Ziare,Stiri,Romania'
oldest_article = 5
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
remove_javascript = True
cover_url = 'http://upload.wikimedia.org/wikipedia/en/d/d6/Logo_noul_adevarul.png'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [ dict(name='div', attrs={'class':'article_header'})
,dict(name='div', attrs={'class':'bd'})
]
remove_tags = [ dict(name='div', attrs={'class':'bb-wg-article_related_attachements'})
,dict(name='div', attrs={'class':'bb-md bb-md-article_comments'})
,dict(name='form', attrs={'id':'bb-comment-create-form'})
]
remove_tags_after = [ dict(name='form', attrs={'id':'bb-comment-create-form'}) ]
feeds = [ (u'\u0218tiri', u'http://www.adevarul.ro/rss/latest') ]
def preprocess_html(self, soup):
return self.adeify_images(soup)

0
resources/recipes/aprospect.recipe Executable file → Normal file
View File

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
capital.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Capital(BasicNewsRecipe):
title = 'Capital'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
remove_javascript = True
publisher = 'Capital'
cover_url = 'http://www.mediapress.ro/imagini/sigla-capital-s16.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [ dict(name='div', attrs={'class':'single one_article'})
]
remove_tags = [ dict(name='div', attrs={'class':'single_details'})
, dict(name='div', attrs={'class':'tx-addoceansbanners-pi1'})
]
feeds = [(u'\u0218tiri', u'http://www.capital.ro/rss.html') ]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
catavencu.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Catavencu(BasicNewsRecipe):
title = u'Academia Ca\u0163avencu'
__author__ = u'Silviu Cotoar\u0103'
description = 'Tagma cum laude'
publisher = 'Catavencu'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare'
encoding = 'utf-8'
cover_url = 'http://upload.wikimedia.org/wikipedia/en/1/1e/Academia_Catavencu.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='ul', attrs={'class':'articles'})
]
remove_tags = [
dict(name='div', attrs={'class':['tools']})
, dict(name='div', attrs={'class':['share']})
, dict(name='div', attrs={'class':['category']})
, dict(name='div', attrs={'id':['comments']})
]
remove_tags_after = [
dict(name='div', attrs={'id':'comments'})
]
feeds = [
(u'Feeds', u'http://catavencu.ro/feed/rss')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
gandul.info
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Gandul(BasicNewsRecipe):
title = u'G\u00E2ndul'
__author__ = u'Silviu Cotoar\u0103'
publisher = 'Gandul'
description = 'Cotidian Online'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://storage0.dms.mpinteractiv.ro/media/1/1/1706/1064063/1/logo.jpg?width=400'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'class':'article'})
]
remove_tags = [
dict(name='a', attrs={'class':'photo'})
, dict(name='div', attrs={'class':'ad'})
]
feeds = [
(u'\u0218tiri', u'http://www.gandul.info/rss-stiri-prima-pagina.xml')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
hotnews.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Hotnews(BasicNewsRecipe):
title = 'Hotnews'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
publisher = 'Hotnews'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://www.hotnews.ro/images/new/logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='h1', attrs={'class':'title'})
,dict(name='div', attrs={'id':'articleContent'})
]
feeds = [ (u'\u0218tiri', u'http://www.hotnews.ro/rss/actualitate')
,(u'English', u'http://www.hotnews.ro/rss/english')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
jurnalul.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class JurnalulNational(BasicNewsRecipe):
title = u'Jurnalul Na\u0163ional'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
publisher = 'Jurnalul National'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://www.jurnalul.ro/images/sigla.png'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='h1', attrs={'class':'h3 art_title'})
,dict(name='div', attrs={'class':'only_text'})
]
feeds = [
(u'\u0218tiri', u'http://www.jurnalul.ro/rss/stiri-3028.html')
,(u'Special', u'http://www.jurnalul.ro/rss/special-3001.html')
,(u'Sport', u'http://www.jurnalul.ro/rss/sport-3035.html')
,(u'Bani Afaceri', u'http://www.jurnalul.ro/rss/bani-afaceri-3006.html')
,(u'Viata Sanatoasa', u'http://www.jurnalul.ro/rss/viata-sanatoasa-3010.html')
,(u'Stiinta Tehnica', u'http://www.jurnalul.ro/rss/stiinta-tehnica-3019.html')
,(u'Timp Liber', u'http://www.jurnalul.ro/rss/timp-liber-3022.html')
,(u'Fun', u'http://www.jurnalul.ro/rss/fun-3038.html')
,(u'Acum 20 de ani', u'http://www.jurnalul.ro/rss/acum-20-de-ani-3073.html')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
mediafax.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Mediafax(BasicNewsRecipe):
title = 'Mediafax'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
publisher = 'Mediafax'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://storage0.dms.mpinteractiv.ro/media/1/1/1706/4134575/2/logo-mediafax-mass-media-news.jpg?width=400'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'class':'news tabs-container'})
]
remove_tags = [
dict(name='ul', attrs={'class':['CategoryNews']})
,dict(name='div', attrs={'class':['read']})
]
remove_tags_after = [ dict(name='div', attrs={'class':'cmsItemViews'}) ]
feeds = [
(u'Actualitate', u'http://www.mediafax.ro/rss/')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
money.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class MoneyRo(BasicNewsRecipe):
title = 'Money Ro'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Rom\u00e2nia'
publisher = 'MoneyRo'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
remove_javascript = True
cover_url = 'http://assets.moneyweb.ro/images/logo_money.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [ dict(name='div', attrs={'id':'titluArticol'})
, dict(name='img', attrs={'id':'objImage'})
, dict(name='div', attrs={'class':'leftColumnArticle'})
]
remove_tags_after = [ dict(name='div', attrs={'id':'articleTags'}) ]
remove_tags = [ dict(name='div', attrs={'id':'ads'})
, dict(name='div', attrs={'id':'aus'})
, dict(name='div', attrs={'id':'bb-comment-create-form'})
, dict(name='div', attrs={'id':'articleTags'})
, dict(name='div', attrs={'class':'breadcrumb'})
]
feeds = [(u'\u0218tiri', u'http://moneyro.feedsportal.com/c/32533/fe.ed/rss.money.ro/stiri.xml') ]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -38,7 +38,7 @@ class Pagina12(BasicNewsRecipe):
keep_only_tags = [dict(attrs={'class':'uitstekendekeus'})] keep_only_tags = [dict(attrs={'class':'uitstekendekeus'})]
remove_tags = [ remove_tags = [
dict(name=['meta','base','link','object','embed']) dict(name=['meta','base','link','object','embed'])
,dict(attrs={'class':['reclamespace','tags-and-sharing']}) ,dict(attrs={'class':['reclamespace','tags-and-sharing','sharing-is-caring']})
] ]
remove_attributes=['lang'] remove_attributes=['lang']

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
prosport.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Prosport(BasicNewsRecipe):
title = 'Prosport'
__author__ = u'Silviu Cotoar\u0103'
publisher = 'Prosport'
description = u'\u0218tiri Sportive din Rom\u00e2nia'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania,Sport'
encoding = 'utf-8'
cover_url = 'http://storage0.dms.mpinteractiv.ro/media/401/581/7946/3688311/1/logo-pro.jpg?width=610'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='h1', attrs={'class':'a-title'})
,dict(name='div', attrs={'class':'a-entry'})
]
remove_tags = [ dict(name='div', attrs={'class':'utils'})
,dict(name='div', attrs={'class':'g-slide'})
]
feeds = [ (u'\u0218tiri', u'http://www.prosport.ro/rss.xml')]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
realitatea.net
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Realitatea(BasicNewsRecipe):
title = 'Realitatea'
__author__ = u'Silviu Cotoar\u0103'
publisher = 'Realitatea'
description = u'\u0218tiri din Rom\u00e2nia'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://assets.realitatea.ro/images/logo.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'class':'articleTitle '})
,dict(name='div', attrs={'class':'articleBody'})
]
remove_tags = [ dict(name='div', attrs={'id':'aus'}) ]
feeds = [ (u'\u0218tiri', u'http://realitatea.feedsportal.com/c/32533/fe.ed/rss.realitatea.net/stiri.xml') ]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
standard.money.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class StandardMoneyRo(BasicNewsRecipe):
title = 'Standard Money Ro'
__author__ = u'Silviu Cotoar\u0103'
publisher = 'Standard Money'
description = 'Portal de Business'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://assets.standard.ro/wp-content/themes/standard/images/standard-logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='h1', attrs={'class':'post-title'})
, dict(name='div', attrs={'class':'content_post'})
]
feeds = [
(u'Actualitate', u'http://standard.money.ro/feed')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
zf.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ZiarulFinanciar(BasicNewsRecipe):
title = 'Ziarul Financiar'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u0218tiri din Business'
publisher = 'Ziarul Financiar'
oldest_article = 5
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Romania'
encoding = 'utf-8'
cover_url = 'http://storage0.dms.mpinteractiv.ro/media/1/1/1706/7462721/1/ziarul-financiar-big.jpg?width=400'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'class':'article'})
]
feeds = [
(u'\u0218tiri', u'http://www.zf.ro/rss/zf-24/')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -24,6 +24,7 @@ from calibre.utils.logging import Log
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
from PIL import Image as PILImage from PIL import Image as PILImage
from lxml import etree
if isosx: if isosx:
try: try:
@ -2515,23 +2516,23 @@ class ITUNES(DriverBase):
fnames = zf_opf.namelist() fnames = zf_opf.namelist()
opf = [x for x in fnames if '.opf' in x][0] opf = [x for x in fnames if '.opf' in x][0]
if opf: if opf:
opf_raw = cStringIO.StringIO(zf_opf.read(opf)) opf_tree = etree.fromstring(zf_opf.read(opf))
soup = BeautifulSoup(opf_raw.getvalue()) ns_map = opf_tree.nsmap.keys()
opf_raw.close() for item in ns_map:
ns = opf_tree.nsmap[item]
# Touch existing calibre timestamp md_el = opf_tree.find(".//{%s}metadata" % ns)
md = soup.find('metadata') if md_el is not None:
if md: ts = md_el.find('.//{%s}meta[@name="calibre:timestamp"]')
ts = md.find('meta',attrs={'name':'calibre:timestamp'}) if ts:
if ts: timestamp = ts.get('content')
timestamp = ts['content'] old_ts = parse_date(timestamp)
old_ts = parse_date(timestamp) metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour, old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo)
old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo) else:
else: metadata.timestamp = now()
metadata.timestamp = now() if DEBUG:
if DEBUG: self.log.info(" add timestamp: %s" % metadata.timestamp)
self.log.info(" add timestamp: %s" % metadata.timestamp) break
else: else:
metadata.timestamp = now() metadata.timestamp = now()
if DEBUG: if DEBUG:
@ -2839,7 +2840,7 @@ class ITUNES(DriverBase):
def _xform_metadata_via_plugboard(self, book, format): def _xform_metadata_via_plugboard(self, book, format):
''' Transform book metadata from plugboard templates ''' ''' Transform book metadata from plugboard templates '''
if DEBUG: if DEBUG:
self.log.info(" ITUNES._update_metadata_from_plugboard()") self.log.info(" ITUNES._xform_metadata_via_plugboard()")
if self.plugboard_func: if self.plugboard_func:
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards) pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)

View File

@ -25,7 +25,7 @@ class APNXBuilder(object):
with open(mobi_file_path, 'rb') as mf: with open(mobi_file_path, 'rb') as mf:
ident = PdbHeaderReader(mf).identity() ident = PdbHeaderReader(mf).identity()
if ident != 'BOOKMOBI': if ident != 'BOOKMOBI':
raise Exception(_('Not a valid MOBI file. Reports identity of %s' % ident)) raise Exception(_('Not a valid MOBI file. Reports identity of %s') % ident)
# Get the pages depending on the chosen parser # Get the pages depending on the chosen parser
pages = [] pages = []

View File

@ -825,7 +825,7 @@ class BooksModel(QAbstractTableModel): # {{{
return False return False
val = int(value.toInt()[0]) if column == 'rating' else \ val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column in ('timestamp', 'pubdate') else \ value.toDate() if column in ('timestamp', 'pubdate') else \
unicode(value.toString()) unicode(value.toString()).strip()
id = self.db.id(row) id = self.db.id(row)
books_to_refresh = set([id]) books_to_refresh = set([id])
if column == 'rating': if column == 'rating':

View File

@ -551,8 +551,10 @@ class BooksView(QTableView): # {{{
return mods & Qt.ControlModifier or mods & Qt.ShiftModifier return mods & Qt.ControlModifier or mods & Qt.ShiftModifier
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and not self.event_has_mods(): ep = event.pos()
self.drag_start_pos = event.pos() if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \
event.button() == Qt.LeftButton and not self.event_has_mods():
self.drag_start_pos = ep
return QTableView.mousePressEvent(self, event) return QTableView.mousePressEvent(self, event)
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):

View File

@ -10,13 +10,15 @@ from PyQt4.Qt import QApplication
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
CommaSeparatedList CommaSeparatedList
from calibre.gui2.preferences.search_ui import Ui_Form from calibre.gui2.preferences.search_ui import Ui_Form
from calibre.gui2 import config from calibre.gui2 import config, error_dialog
from calibre.utils.config import prefs from calibre.utils.config import prefs
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui): def genesis(self, gui):
self.gui = gui self.gui = gui
db = gui.library_view.model().db
self.db = db
r = self.register r = self.register
@ -24,11 +26,153 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('highlight_search_matches', config) r('highlight_search_matches', config)
r('limit_search_columns', prefs) r('limit_search_columns', prefs)
r('limit_search_columns_to', prefs, setting=CommaSeparatedList) r('limit_search_columns_to', prefs, setting=CommaSeparatedList)
fl = gui.library_view.model().db.field_metadata.get_search_terms() fl = db.field_metadata.get_search_terms()
self.opt_limit_search_columns_to.update_items_cache(fl) self.opt_limit_search_columns_to.update_items_cache(fl)
self.clear_history_button.clicked.connect(self.clear_histories) self.clear_history_button.clicked.connect(self.clear_histories)
self.gst_explanation.setText('<p>' + _(
"<b>Grouped search terms</b> are search names that permit a query to automatically "
"search across more than one column. For example, if you create a grouped "
"search term <code>allseries</code> with the value "
"<code>series, #myseries, #myseries2</code>, then "
"the query <code>allseries:adhoc</code> will find 'adhoc' in any of the "
"columns <code>series</code>, <code>#myseries</code>, and "
"<code>#myseries2</code>.<p> Enter the name of the "
"grouped search term in the drop-down box, enter the list of columns "
"to search in the value box, then push the Save button. "
"<p>Note: Search terms are forced to lower case; <code>MySearch</code> "
"and <code>mysearch</code> are the same term."
"<p>You can have your grouped search term show up as user categories in "
" the Tag Browser. Just add the grouped search term names to the Make user "
"categories from box. You can add multiple terms separated by commas. "
"The new user category will be automatically "
"populated with all the items in the categories included in the grouped "
"search term. <p>Automatic user categories permit you to see easily "
"all the category items that "
"are in the columns contained in the grouped search term. Using the above "
"<code>allseries</code> example, the automatically-generated user category "
"will contain all the series mentioned in <code>series</code>, "
"<code>#myseries</code>, and <code>#myseries2</code>. This "
"can be useful to check for duplicates, to find which column contains "
"a particular item, or to have hierarchical categories (categories "
"that contain categories)."))
self.gst = db.prefs.get('grouped_search_terms', {})
self.orig_gst_keys = self.gst.keys()
fl = []
for f in db.all_field_keys():
fm = db.metadata_for_field(f)
if not fm['search_terms']:
continue
if not fm['is_category']:
continue
fl.append(f)
self.gst_value.update_items_cache(fl)
self.fill_gst_box(select=None)
self.gst_delete_button.setEnabled(False)
self.gst_save_button.setEnabled(False)
self.gst_names.currentIndexChanged[int].connect(self.gst_index_changed)
self.gst_names.editTextChanged.connect(self.gst_text_changed)
self.gst_value.textChanged.connect(self.gst_text_changed)
self.gst_save_button.clicked.connect(self.gst_save_clicked)
self.gst_delete_button.clicked.connect(self.gst_delete_clicked)
self.gst_changed = False
if db.prefs.get('grouped_search_make_user_categories', None) is None:
db.prefs.set('grouped_search_make_user_categories', [])
r('grouped_search_make_user_categories', db.prefs, setting=CommaSeparatedList)
self.muc_changed = False
self.opt_grouped_search_make_user_categories.editingFinished.connect(
self.muc_box_changed)
def muc_box_changed(self):
self.muc_changed = True
def gst_save_clicked(self):
idx = self.gst_names.currentIndex()
name = icu_lower(unicode(self.gst_names.currentText()))
if not name:
return error_dialog(self.gui, _('Grouped Search Terms'),
_('The search term cannot be blank'),
show=True)
if idx != 0:
orig_name = unicode(self.gst_names.itemData(idx).toString())
else:
orig_name = ''
if name != orig_name:
if name in self.db.field_metadata.get_search_terms() and \
name not in self.orig_gst_keys:
return error_dialog(self.gui, _('Grouped Search Terms'),
_('That name is already used for a column or grouped search term'),
show=True)
if name in [icu_lower(p) for p in self.db.prefs.get('user_categories', {})]:
return error_dialog(self.gui, _('Grouped Search Terms'),
_('That name is already used for user category'),
show=True)
val = [v.strip() for v in unicode(self.gst_value.text()).split(',') if v.strip()]
if not val:
return error_dialog(self.gui, _('Grouped Search Terms'),
_('The value box cannot be empty'), show=True)
if orig_name and name != orig_name:
del self.gst[orig_name]
self.gst_changed = True
self.gst[name] = val
self.fill_gst_box(select=name)
self.changed_signal.emit()
def gst_delete_clicked(self):
if self.gst_names.currentIndex() == 0:
return error_dialog(self.gui, _('Grouped Search Terms'),
_('The empty grouped search term cannot be deleted'), show=True)
name = unicode(self.gst_names.currentText())
if name in self.gst:
del self.gst[name]
self.fill_gst_box(select='')
self.changed_signal.emit()
self.gst_changed = True
def fill_gst_box(self, select=None):
terms = sorted(self.gst.keys())
self.opt_grouped_search_make_user_categories.update_items_cache(terms)
self.gst_names.blockSignals(True)
self.gst_names.clear()
self.gst_names.addItem('', '')
for t in terms:
self.gst_names.addItem(t, t)
self.gst_names.blockSignals(False)
if select is not None:
if select == '':
self.gst_index_changed(0)
elif select in terms:
self.gst_names.setCurrentIndex(self.gst_names.findText(select))
def gst_text_changed(self):
self.gst_delete_button.setEnabled(False)
self.gst_save_button.setEnabled(True)
def gst_index_changed(self, idx):
self.gst_delete_button.setEnabled(idx != 0)
self.gst_save_button.setEnabled(False)
self.gst_value.blockSignals(True)
if idx == 0:
self.gst_value.setText('')
else:
name = unicode(self.gst_names.itemData(idx).toString())
self.gst_value.setText(','.join(self.gst[name]))
self.gst_value.blockSignals(False)
def commit(self):
if self.gst_changed:
self.db.prefs.set('grouped_search_terms', self.gst)
self.db.field_metadata.add_grouped_search_terms(self.gst)
return ConfigWidgetBase.commit(self)
def refresh_gui(self, gui): def refresh_gui(self, gui):
if self.muc_changed:
gui.tags_view.set_new_model()
gui.search.search_as_you_type(config['search_as_you_type']) gui.search.search_as_you_type(config['search_as_you_type'])
gui.library_view.model().set_highlight_only(config['highlight_search_matches']) gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
gui.search.do_search() gui.search.do_search()

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>670</width> <width>670</width>
<height>392</height> <height>556</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -77,19 +77,6 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QPushButton" name="clear_history_button"> <widget class="QPushButton" name="clear_history_button">
<property name="toolTip"> <property name="toolTip">
@ -100,6 +87,120 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Grouped Search Terms</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QHBoxLayout" name="l12">
<item>
<widget class="QLabel" name="la10">
<property name="text">
<string>&amp;Names:</string>
</property>
<property name="buddy">
<cstring>gst_names</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="gst_names">
<property name="editable">
<bool>true</bool>
</property>
<property name="minimumContentsLength">
<number>10</number>
</property>
<property name="toolTip">
<string>Contains the names of the currently-defined group search terms.
Create a new name by entering it into the empty box, then
pressing Save. Rename a search term by selecting it then
changing the name and pressing Save. Change the value of
a search term by changing the value box then pressing Save.</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="gst_delete_button">
<property name="toolTip">
<string>Delete the current search term</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="MultiCompleteLineEdit" name="gst_value"/>
</item>
<item>
<widget class="QToolButton" name="gst_save_button">
<property name="toolTip">
<string>Save the current search term. You can rename a search term by
changing the name then pressing Save. You can change the value
of a search term by changing the value box then pressing Save.</string>
</property>
<property name="text">
<string>&amp;Save</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QTextBrowser" name="gst_explanation">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="l11">
<property name="text">
<string>Make &amp;user categories from:</string>
</property>
<property name="buddy">
<cstring>opt_grouped_search_make_user_categories</cstring>
</property>
</widget>
</item>
<item>
<widget class="MultiCompleteLineEdit" name="opt_grouped_search_make_user_categories">
<property name="toolTip">
<string>Enter the names of any grouped search terms you wish
to be shown as user categories</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>
@ -109,6 +210,8 @@
<header>calibre/gui2/complete.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -466,10 +466,7 @@ class TagTreeItem(object): # {{{
icon_map[0] = data.icon icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
if tooltip: if tooltip:
if tooltip.endswith(':'): self.tooltip = tooltip + ' '
self.tooltip = tooltip + ' '
else:
self.tooltip = tooltip + ': '
else: else:
self.tooltip = '' self.tooltip = ''
@ -589,11 +586,17 @@ class TagsModel(QAbstractItemModel): # {{{
# get_node_tree cannot return None here, because row_map is empty # get_node_tree cannot return None here, because row_map is empty
data = self.get_node_tree(config['sort_tags_by']) data = self.get_node_tree(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem() self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
tt = _(u'The lookup/search name is "{0}"').format(r) if r.startswith('@') and r[1:] in gst:
tt = _(u'The grouped search term name is "{0}"').format(r[1:])
elif r == 'news':
tt = ''
else:
tt = _(u'The lookup/search name is "{0}"').format(r)
TagTreeItem(parent=self.root_item, TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
@ -735,6 +738,14 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map = [] self.row_map = []
self.categories = [] self.categories = []
# Get the categories
if self.search_restriction:
data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
# Reconstruct the user categories, putting them into metadata # Reconstruct the user categories, putting them into metadata
self.db.field_metadata.remove_dynamic_categories() self.db.field_metadata.remove_dynamic_categories()
tb_cats = self.db.field_metadata tb_cats = self.db.field_metadata
@ -746,17 +757,16 @@ class TagsModel(QAbstractItemModel): # {{{
except ValueError: except ValueError:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
for cat in sorted(self.db.prefs.get('grouped_search_terms', {}),
key=sort_key):
if (u'@' + cat) in data:
tb_cats.add_user_category(label=u'@' + cat, name=cat)
self.db.data.change_search_locations(self.db.field_metadata.get_search_terms())
if len(saved_searches().names()): if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches')) tb_cats.add_search_category(label='search', name=_('Searches'))
# Now get the categories
if self.search_restriction:
data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
if self.filter_categories_by: if self.filter_categories_by:
for category in data.keys(): for category in data.keys():
data[category] = [t for t in data[category] data[category] = [t for t in data[category]
@ -767,6 +777,7 @@ class TagsModel(QAbstractItemModel): # {{{
if category in data: # The search category can come and go if category in data: # The search category can come and go
self.row_map.append(category) self.row_map.append(category)
self.categories.append(tb_categories[category]['name']) self.categories.append(tb_categories[category]['name'])
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
# A category has been added or removed. We must force a rebuild of # A category has been added or removed. We must force a rebuild of
# the model # the model
@ -822,6 +833,7 @@ class TagsModel(QAbstractItemModel): # {{{
not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user' \ not self.db.field_metadata[r]['kind'] == 'user' \
else False else False
tt = r if self.db.field_metadata[r]['kind'] == 'user' else None
for idx,tag in enumerate(data[r]): for idx,tag in enumerate(data[r]):
if clear_rating: if clear_rating:
tag.avg_rating = None tag.avg_rating = None
@ -861,10 +873,10 @@ class TagsModel(QAbstractItemModel): # {{{
category_icon = category_node.icon, category_icon = category_node.icon,
tooltip = None, tooltip = None,
category_key=category_node.category_key) category_key=category_node.category_key)
t = TagTreeItem(parent=sub_cat, data=tag, tooltip=r, t = TagTreeItem(parent=sub_cat, data=tag, tooltip=tt,
icon_map=self.icon_state_map) icon_map=self.icon_state_map)
else: else:
t = TagTreeItem(parent=category, data=tag, tooltip=r, t = TagTreeItem(parent=category, data=tag, tooltip=tt,
icon_map=self.icon_state_map) icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()
return True return True

View File

@ -433,6 +433,10 @@ class ResultCache(SearchQueryParser): # {{{
if len(candidates) == 0: if len(candidates) == 0:
return matches return matches
if len(location) > 2 and location.startswith('@') and \
location[1:] in self.db_prefs['grouped_search_terms']:
location = location[1:]
if query and query.strip(): if query and query.strip():
# get metadata key associated with the search term. Eliminates # get metadata key associated with the search term. Eliminates
# dealing with plurals and other aliases # dealing with plurals and other aliases
@ -440,9 +444,16 @@ class ResultCache(SearchQueryParser): # {{{
# grouped search terms # grouped search terms
if isinstance(location, list): if isinstance(location, list):
if allow_recursion: if allow_recursion:
if query.lower() == 'false':
invert = True
query = 'true'
else:
invert = False
for loc in location: for loc in location:
matches |= self.get_matches(loc, query, matches |= self.get_matches(loc, query,
candidates=candidates, allow_recursion=False) candidates=candidates, allow_recursion=False)
if invert:
matches = self.universal_set() - matches
return matches return matches
raise ParseException(query, len(query), 'Recursive query group detected', self) raise ParseException(query, len(query), 'Recursive query group detected', self)

View File

@ -1481,23 +1481,36 @@ class EPUB_MOBI(CatalogPlugin):
current_author = authors[0] current_author = authors[0]
for (i,author) in enumerate(authors): for (i,author) in enumerate(authors):
if author != current_author and i: if author != current_author and i:
# Exit if author matches previous, but author_sort doesn't match
if author[0] == current_author[0]: if author[0] == current_author[0]:
error_msg = _(''' if self.opts.fmt == 'mobi':
Inconsistent Author Sort values for Author '{0}': # Exit if building MOBI
'{1}' <> '{2}', error_msg = _(
unable to build catalog.\n '''Inconsistent Author Sort values for
Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog, Author '{0}':
then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) '{1}' <> '{2}'
self.opts.log.warn('\n*** Metadata error ***') Unable to build MOBI catalog.\n
self.opts.log.warn(error_msg) Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog, then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
self.opts.log.warn('\n*** Metadata error ***')
self.opts.log.warn(error_msg)
self.error.append('Author Sort mismatch')
self.error.append(error_msg)
return False
else:
# Warning if building non-MOBI
if not self.error:
self.error.append('Author Sort mismatch')
error_msg = _(
'''Warning: inconsistent Author Sort values for
Author '{0}':
'{1}' <> '{2}'\n''').format(author[0],author[1],current_author[1])
self.opts.log.warn('\n*** Metadata warning ***')
self.opts.log.warn(error_msg)
self.error.append(error_msg)
self.error.append('Metadata error')
self.error.append(error_msg)
return False
current_author = author current_author = author
self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author_sort) self.booksByAuthor = sorted(self.booksByAuthor, key=self.booksByAuthorSorter_author_sort)
# Build the unique_authors set from existing data # Build the unique_authors set from existing data
@ -2135,7 +2148,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1])
if author_count == 1: if author_count == 1:
divOpeningTag.insert(dotc, pBookTag) divOpeningTag.insert(dotc, pBookTag)
dotc += 1 dotc += 1
else: elif divRunningTag:
divRunningTag.insert(drtc,pBookTag) divRunningTag.insert(drtc,pBookTag)
drtc += 1 drtc += 1

View File

@ -188,6 +188,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
migrate_preference('saved_searches', {}) migrate_preference('saved_searches', {})
set_saved_searches(self, 'saved_searches') set_saved_searches(self, 'saved_searches')
# migrate grouped_search_terms
if self.prefs.get('grouped_search_terms', None) is None:
try:
ogst = tweaks.get('grouped_search_terms', {})
ngst = {}
for t in ogst:
ngst[icu_lower(t)] = ogst[t]
self.prefs.set('grouped_search_terms', ngst)
except:
pass
# Rename any user categories with names that differ only in case # Rename any user categories with names that differ only in case
user_cats = self.prefs.get('user_categories', []) user_cats = self.prefs.get('user_categories', [])
catmap = {} catmap = {}
@ -349,12 +360,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if len(saved_searches().names()): if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches')) tb_cats.add_search_category(label='search', name=_('Searches'))
gst = tweaks['grouped_search_terms'] self.field_metadata.add_grouped_search_terms(
for t in gst: self.prefs.get('grouped_search_terms', {}))
try:
self.field_metadata._add_search_terms_to_map(gst[t], [t])
except ValueError:
traceback.print_exc()
self.book_on_device_func = None self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs) self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs)
@ -1293,7 +1300,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# icon_map is not None if get_categories is to store an icon and # icon_map is not None if get_categories is to store an icon and
# possibly a tooltip in the tag structure. # possibly a tooltip in the tag structure.
icon = None icon = None
tooltip = '' tooltip = '(' + category + ')'
label = tb_cats.key_to_label(category) label = tb_cats.key_to_label(category)
if icon_map: if icon_map:
if not tb_cats.is_custom_field(category): if not tb_cats.is_custom_field(category):
@ -1379,7 +1386,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories['formats'].sort(key = lambda x:x.name) categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. #### #### Now do the user-defined categories. ####
user_categories = self.prefs['user_categories'] user_categories = dict.copy(self.prefs['user_categories'])
# We want to use same node in the user category as in the source # We want to use same node in the user category as in the source
# category. To do that, we need to find the original Tag node. There is # category. To do that, we need to find the original Tag node. There is
@ -1390,6 +1397,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for c in categories.keys(): for c in categories.keys():
taglist[c] = dict(map(lambda t:(t.name, t), categories[c])) taglist[c] = dict(map(lambda t:(t.name, t), categories[c]))
muc = self.prefs.get('grouped_search_make_user_categories', [])
gst = self.prefs.get('grouped_search_terms', {})
for c in gst:
if c not in muc:
continue
user_categories[c] = []
for sc in gst[c]:
if sc in categories.keys():
for t in categories[sc]:
user_categories[c].append([t.name, sc, 0])
for user_cat in sorted(user_categories.keys(), key=sort_key): for user_cat in sorted(user_categories.keys(), key=sort_key):
items = [] items = []
for (name,label,ign) in user_categories[user_cat]: for (name,label,ign) in user_categories[user_cat]:

View File

@ -3,7 +3,7 @@ Created on 25 May 2010
@author: charles @author: charles
''' '''
import copy import copy, traceback
from calibre.utils.ordered_dict import OrderedDict from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -488,6 +488,20 @@ class FieldMetadata(dict):
del self._search_term_map[k] del self._search_term_map[k]
del self._tb_cats[key] del self._tb_cats[key]
def _remove_grouped_search_terms(self):
to_remove = [v for v in self._search_term_map
if isinstance(self._search_term_map[v], list)]
for v in to_remove:
del self._search_term_map[v]
def add_grouped_search_terms(self, gst):
self._remove_grouped_search_terms()
for t in gst:
try:
self._add_search_terms_to_map(gst[t], [t])
except ValueError:
traceback.print_exc()
def cc_series_index_column_for(self, key): def cc_series_index_column_for(self, key):
return self._tb_cats[key]['rec_index'] + 1 return self._tb_cats[key]['rec_index'] + 1

File diff suppressed because it is too large Load Diff