mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Pull from trunk
This commit is contained in:
commit
d4f47b8e75
@ -4,6 +4,45 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.1
|
||||||
|
date: 2010-06-04
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Content server: Add option to control category groupiong in OPDS feeds"
|
||||||
|
|
||||||
|
- title: "Make the book details pane occupy the full lower part of the window"
|
||||||
|
|
||||||
|
- title: "Add true and false searches for date based columns"
|
||||||
|
tickets: [5717]
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "iPad driver: Various bug fixes."
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix Launcher partition being detected as storage card in linux"
|
||||||
|
|
||||||
|
- title: "Fix news downloading breaking on windows systems with local encoding other than UTF-8."
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix problem caused by null titles"
|
||||||
|
|
||||||
|
- title: "Make the new splash screen not always stay on top"
|
||||||
|
tickets: [5700]
|
||||||
|
|
||||||
|
- title: "When setting an image with transparent pixels as the book cover, overlay it on a white background first. Fixes transparent covers getting random backgrounds."
|
||||||
|
|
||||||
|
- title: "Content server: Fix stanza integration when entering the server URL my hand"
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Gizmodo
|
||||||
|
- Vreme
|
||||||
|
|
||||||
|
|
||||||
|
- version: 0.7.0
|
||||||
|
date: 2010-06-04
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Go to http://calibre-ebook.com/new-in/seven to see what's new in 0.7.0"
|
||||||
|
type: major
|
||||||
|
|
||||||
- version: 0.6.55
|
- version: 0.6.55
|
||||||
date: 2010-05-28
|
date: 2010-05-28
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 820 B |
BIN
resources/images/news/haaretz_en.png
Normal file
BIN
resources/images/news/haaretz_en.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 712 B |
25
resources/recipes/cbc_canada.recipe
Normal file
25
resources/recipes/cbc_canada.recipe
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1275798572(BasicNewsRecipe):
|
||||||
|
title = u'CBC Canada'
|
||||||
|
publisher = 'www.cbc.ca'
|
||||||
|
language = 'en_CA'
|
||||||
|
__author__ = 'rty'
|
||||||
|
category = 'news'
|
||||||
|
oldest_article = 4
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
masthead_url = 'http://www.cbc.ca/includes/gfx/cbcnews_logo_09.gif'
|
||||||
|
cover_url = 'http://img692.imageshack.us/img692/2814/cbc.png'
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':['storyhead','storybody']})]
|
||||||
|
remove_tags_after = dict(id=['socialtools'])
|
||||||
|
feeds = [(u'Top Stories', u'http://rss.cbc.ca/lineup/topstories.xml'),
|
||||||
|
(u'World', u'http://rss.cbc.ca/lineup/world.xml'),
|
||||||
|
(u'National', u'http://rss.cbc.ca/lineup/canada.xml'),
|
||||||
|
(u'Manitoba', u'http://rss.cbc.ca/lineup/canada-manitoba.xml'),
|
||||||
|
(u'Politics', u'http://rss.cbc.ca/lineup/politics.xml'),
|
||||||
|
(u'Tech & Science', u'http://rss.cbc.ca/lineup/technology.xml'),
|
||||||
|
(u'Books', u'http://rss.cbc.ca/lineup/arts-books.xml')]
|
@ -5,7 +5,6 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
|||||||
clarin.com
|
clarin.com
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from calibre import strftime
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class Clarin(BasicNewsRecipe):
|
class Clarin(BasicNewsRecipe):
|
||||||
@ -18,11 +17,12 @@ class Clarin(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
cover_url = strftime('http://www.clarin.com/diario/%Y/%m/%d/portada.jpg')
|
encoding = 'utf8'
|
||||||
encoding = 'cp1252'
|
language = 'es_AR'
|
||||||
language = 'es'
|
publication_type = 'newspaper'
|
||||||
masthead_url = 'http://www.clarin.com/shared/v10/img/Hd/lg_Clarin.gif'
|
INDEX = 'http://www.clarin.com'
|
||||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,"Times New Roman",Times,serif; font-size: xx-large} .Volan,.Pie,.Autor{ font-size: x-small} .Copete,.Hora{font-size: large} '
|
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
||||||
|
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} '
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -31,27 +31,32 @@ class Clarin(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_tags = [
|
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
|
||||||
dict(name='a' , attrs={'class':'Imp' })
|
|
||||||
,dict(name='div' , attrs={'class':'Perma' })
|
|
||||||
,dict(name='h1' , text='Imprimir' )
|
|
||||||
]
|
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Ultimo Momento', u'http://www.clarin.com/diario/hoy/um/sumariorss.xml')
|
(u'Pagina principal', u'http://www.clarin.com/rss/' )
|
||||||
,(u'El Pais' , u'http://www.clarin.com/diario/hoy/elpais.xml' )
|
,(u'Politica' , u'http://www.clarin.com/rss/politica/' )
|
||||||
,(u'Opinion' , u'http://www.clarin.com/diario/hoy/opinion.xml' )
|
,(u'Deportes' , u'http://www.clarin.com/rss/deportes/' )
|
||||||
,(u'El Mundo' , u'http://www.clarin.com/diario/hoy/elmundo.xml' )
|
,(u'Economia' , u'http://www.clarin.com/economia/' )
|
||||||
,(u'Sociedad' , u'http://www.clarin.com/diario/hoy/sociedad.xml' )
|
,(u'Mundo' , u'http://www.clarin.com/rss/mundo/' )
|
||||||
,(u'La Ciudad' , u'http://www.clarin.com/diario/hoy/laciudad.xml' )
|
,(u'Espectaculos' , u'http://www.clarin.com/rss/espectaculos/')
|
||||||
,(u'Policiales' , u'http://www.clarin.com/diario/hoy/policiales.xml' )
|
,(u'Sociedad' , u'http://www.clarin.com/rss/sociedad/' )
|
||||||
,(u'Deportes' , u'http://www.clarin.com/diario/hoy/deportes.xml' )
|
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||||
|
,(u'Policiales' , u'http://www.clarin.com/rss/policiales/' )
|
||||||
|
,(u'Internet' , u'http://www.clarin.com/rss/internet/' )
|
||||||
|
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||||
]
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
rest = url.partition('-0')[-1]
|
return url + '?print=1'
|
||||||
lmain = rest.partition('.')[0]
|
|
||||||
lurl = u'http://www.servicios.clarin.com/notas/jsp/clarin/v9/notas/imprimir.jsp?pagid=' + lmain
|
|
||||||
return lurl
|
|
||||||
|
|
||||||
|
def get_cover_url(self):
|
||||||
|
cover_url = None
|
||||||
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
cover_item = soup.find('div',attrs={'class':'bb-md bb-md-edicion_papel'})
|
||||||
|
if cover_item:
|
||||||
|
ap = cover_item.find('a',attrs={'href':'/edicion-impresa/'})
|
||||||
|
if ap:
|
||||||
|
cover_url = self.INDEX + ap.img['src']
|
||||||
|
return cover_url
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class Gizmodo(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
use_embedded_content = True
|
use_embedded_content = False
|
||||||
language = 'en'
|
language = 'en'
|
||||||
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
||||||
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
||||||
@ -30,8 +30,10 @@ class Gizmodo(BasicNewsRecipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['width','height']
|
remove_attributes = ['width','height']
|
||||||
remove_tags = [dict(name='div',attrs={'class':'feedflare'})]
|
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||||
remove_tags_after = dict(name='div',attrs={'class':'feedflare'})
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||||
|
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||||
|
|
||||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
||||||
|
|
||||||
|
57
resources/recipes/haaretz_en.recipe
Normal file
57
resources/recipes/haaretz_en.recipe
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
haaretz.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Haaretz_en(BasicNewsRecipe):
|
||||||
|
title = 'Haaretz in English'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
|
||||||
|
publisher = 'haaretz.com'
|
||||||
|
category = 'news, politics, Israel'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'cp1252'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en_IL'
|
||||||
|
publication_type = 'newspaper'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
|
||||||
|
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
|
||||||
|
remove_tags_before = dict(name='h1')
|
||||||
|
remove_tags_after = dict(attrs={'id':'innerArticle'})
|
||||||
|
keep_only_tags = [dict(attrs={'id':'content'})]
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
|
||||||
|
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
|
||||||
|
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
|
||||||
|
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
|
||||||
|
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
|
||||||
|
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
|
||||||
|
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
|
||||||
|
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
|
||||||
|
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
|
||||||
|
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
|
||||||
|
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
|
||||||
|
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
@ -52,10 +52,12 @@ class Vreme(BasicNewsRecipe):
|
|||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
articles = []
|
articles = []
|
||||||
soup = self.index_to_soup(self.INDEX)
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
cover_item = soup.find('div',attrs={'id':'najava'})
|
||||||
|
if cover_item:
|
||||||
|
self.cover_url = self.INDEX + cover_item.img['src']
|
||||||
for item in soup.findAll(['h3','h4']):
|
for item in soup.findAll(['h3','h4']):
|
||||||
description = ''
|
description = u''
|
||||||
title_prefix = ''
|
title_prefix = u''
|
||||||
feed_link = item.find('a')
|
feed_link = item.find('a')
|
||||||
if feed_link and feed_link.has_key('href') and feed_link['href'].startswith('/cms/view.php'):
|
if feed_link and feed_link.has_key('href') and feed_link['href'].startswith('/cms/view.php'):
|
||||||
url = self.INDEX + feed_link['href']
|
url = self.INDEX + feed_link['href']
|
||||||
@ -67,7 +69,7 @@ class Vreme(BasicNewsRecipe):
|
|||||||
,'url' :url
|
,'url' :url
|
||||||
,'description':description
|
,'description':description
|
||||||
})
|
})
|
||||||
return [(soup.head.title.string, articles)]
|
return [('Nedeljnik Vreme', articles)]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name=['object','link'])
|
dict(name=['object','link'])
|
||||||
@ -76,11 +78,3 @@ class Vreme(BasicNewsRecipe):
|
|||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + '&print=yes'
|
return url + '&print=yes'
|
||||||
|
|
||||||
def get_cover_url(self):
|
|
||||||
cover_url = None
|
|
||||||
soup = self.index_to_soup(self.INDEX)
|
|
||||||
cover_item = soup.find('div',attrs={'id':'najava'})
|
|
||||||
if cover_item:
|
|
||||||
cover_url = self.INDEX + cover_item.img['src']
|
|
||||||
return cover_url
|
|
||||||
|
@ -21,12 +21,16 @@ class weltDe(BasicNewsRecipe):
|
|||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_stylesheets = True
|
remove_stylesheets = True
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
encoding = 'iso-8859-1'
|
encoding = 'utf-8'
|
||||||
BasicNewsRecipe.summary_length = 200
|
html2epub_options = 'linearize_tables = True\nbase_font_size2=10'
|
||||||
|
BasicNewsRecipe.summary_length = 100
|
||||||
|
|
||||||
|
|
||||||
remove_tags = [dict(id='jumplinks'),
|
remove_tags = [dict(id='jumplinks'),
|
||||||
dict(id='ad1'),
|
dict(id='ad1'),
|
||||||
|
dict(id='top'),
|
||||||
|
dict(id='header'),
|
||||||
|
dict(id='additionalNavWrapper'),
|
||||||
dict(id='fullimage_index'),
|
dict(id='fullimage_index'),
|
||||||
dict(id='additionalNav'),
|
dict(id='additionalNav'),
|
||||||
dict(id='printMenu'),
|
dict(id='printMenu'),
|
||||||
@ -35,6 +39,8 @@ class weltDe(BasicNewsRecipe):
|
|||||||
dict(id='servicesBox'),
|
dict(id='servicesBox'),
|
||||||
dict(id='servicesNav'),
|
dict(id='servicesNav'),
|
||||||
dict(id='ad2'),
|
dict(id='ad2'),
|
||||||
|
dict(id='banner_1'),
|
||||||
|
dict(id='ssoInfoTop'),
|
||||||
dict(id='brandingWrapper'),
|
dict(id='brandingWrapper'),
|
||||||
dict(id='links-intern'),
|
dict(id='links-intern'),
|
||||||
dict(id='navigation'),
|
dict(id='navigation'),
|
||||||
@ -53,10 +59,22 @@ class weltDe(BasicNewsRecipe):
|
|||||||
dict(id='xmsg_comment'),
|
dict(id='xmsg_comment'),
|
||||||
dict(id='additionalNavWrapper'),
|
dict(id='additionalNavWrapper'),
|
||||||
dict(id='imagebox'),
|
dict(id='imagebox'),
|
||||||
|
dict(id='footerContainer'),
|
||||||
#dict(id=''),
|
#dict(id=''),
|
||||||
dict(name='span'),
|
dict(name='span'),
|
||||||
dict(name='div', attrs={'class':'printURL'}),
|
dict(name='div', attrs={'class':'printURL'}),
|
||||||
|
dict(name='ul', attrs={'class':'clear mainNavigation inline'}),
|
||||||
|
dict(name='ul', attrs={'class':'inline'}),
|
||||||
|
dict(name='ul', attrs={'class':'ubar'}),
|
||||||
|
dict(name='hr', attrs={'class':'ubar'}),
|
||||||
|
dict(name='li', attrs={'class':'counter'}),
|
||||||
|
dict(name='li', attrs={'class':'browseBack'}),
|
||||||
|
dict(name='li', attrs={'class':'browseNext'}),
|
||||||
|
dict(name='li', attrs={'class':'selected'}),
|
||||||
|
dict(name='div', attrs={'class':'floatLeft'}),
|
||||||
dict(name='div', attrs={'class':'ad'}),
|
dict(name='div', attrs={'class':'ad'}),
|
||||||
|
dict(name='div', attrs={'class':'ftBarLeft'}),
|
||||||
|
dict(name='div', attrs={'class':'clear additionalNav'}),
|
||||||
dict(name='div', attrs={'class':'inlineBox inlineFurtherLinks'}),
|
dict(name='div', attrs={'class':'inlineBox inlineFurtherLinks'}),
|
||||||
dict(name='div', attrs={'class':'inlineBox videoInlineBox'}),
|
dict(name='div', attrs={'class':'inlineBox videoInlineBox'}),
|
||||||
dict(name='div', attrs={'class':'inlineGallery'}),
|
dict(name='div', attrs={'class':'inlineGallery'}),
|
||||||
@ -65,6 +83,23 @@ class weltDe(BasicNewsRecipe):
|
|||||||
dict(name='div', attrs={'class':'articleOptions clear'}),
|
dict(name='div', attrs={'class':'articleOptions clear'}),
|
||||||
dict(name='div', attrs={'class':'noPrint galleryIndex'}),
|
dict(name='div', attrs={'class':'noPrint galleryIndex'}),
|
||||||
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
|
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
|
||||||
|
dict(name='div', attrs={'class':'clear module writeComment bgColor1'}),
|
||||||
|
dict(name='div', attrs={'class':'clear module textGallery bgColor1'}),
|
||||||
|
dict(name='div', attrs={'class':'clear module socialMedia bgColor1'}),
|
||||||
|
dict(name='div', attrs={'class':'clear module continuativeLinks'}),
|
||||||
|
dict(name='div', attrs={'class':'moreArtH3'}),
|
||||||
|
dict(name='div', attrs={'class':'jqmWindow'}),
|
||||||
|
dict(name='div', attrs={'class':'clear gap4'}),
|
||||||
|
dict(name='div', attrs={'class':'hidden'}),
|
||||||
|
dict(name='div', attrs={'class':'advertising'}),
|
||||||
|
dict(name='div', attrs={'class':'ad adMarginBottom'}),
|
||||||
|
dict(name='div', attrs={'class':'ad'}),
|
||||||
|
dict(name='div', attrs={'class':'topLine'}),
|
||||||
|
dict(name='div', attrs={'class':'toplineH2'}),
|
||||||
|
dict(name='div', attrs={'class':'headLineH3'}),
|
||||||
|
dict(name='div', attrs={'class':'print'}),
|
||||||
|
dict(name='div', attrs={'class':'clear menu'}),
|
||||||
|
dict(name='div', attrs={'class':'clear galleryContent'}),
|
||||||
dict(name='p', attrs={'class':'jump'}),
|
dict(name='p', attrs={'class':'jump'}),
|
||||||
dict(name='a', attrs={'class':'commentLink'}),
|
dict(name='a', attrs={'class':'commentLink'}),
|
||||||
dict(name='h2', attrs={'class':'jumpHeading'}),
|
dict(name='h2', attrs={'class':'jumpHeading'}),
|
||||||
@ -75,7 +110,7 @@ class weltDe(BasicNewsRecipe):
|
|||||||
dict(name='table', attrs={'class':'textGallery'}),
|
dict(name='table', attrs={'class':'textGallery'}),
|
||||||
dict(name='li', attrs={'class':'active'})]
|
dict(name='li', attrs={'class':'active'})]
|
||||||
|
|
||||||
remove_tags_after = [dict(id='tw_link_widget')]
|
remove_tags_after = [dict(name='div', attrs={'class':'clear departmentLine'})]
|
||||||
|
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
|
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
|
||||||
@ -87,7 +122,6 @@ class weltDe(BasicNewsRecipe):
|
|||||||
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
|
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
|
||||||
|
|
||||||
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
|
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
|
||||||
('Deutsche Dinge', 'http://www.welt.de/deutsche-dinge/?service=Rss'),
|
|
||||||
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
|
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
|
||||||
('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
|
('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
|
||||||
('Sport', 'http://welt.de/sport/?service=Rss'),
|
('Sport', 'http://welt.de/sport/?service=Rss'),
|
||||||
@ -101,4 +135,5 @@ class weltDe(BasicNewsRecipe):
|
|||||||
|
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url.replace ('.html', '.html?print=yes')
|
return url.replace ('.html', '.html?print=true')
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ mimetypes.add_type('application/vnd.palm', '.pdb')
|
|||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.mobi')
|
||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.prc')
|
||||||
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
mimetypes.add_type('application/x-mobipocket-ebook', '.azw')
|
||||||
|
mimetypes.add_type('application/x-cbz', '.cbz')
|
||||||
|
mimetypes.add_type('application/x-cbr', '.cbr')
|
||||||
mimetypes.add_type('image/wmf', '.wmf')
|
mimetypes.add_type('image/wmf', '.wmf')
|
||||||
guess_type = mimetypes.guess_type
|
guess_type = mimetypes.guess_type
|
||||||
import cssutils
|
import cssutils
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.6.55'
|
__version__ = '0.7.1'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Device driver for the SONY devices
|
Device driver for the SONY devices
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os, time, re
|
||||||
import re
|
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
from calibre.devices.usbms.driver import USBMS
|
||||||
from calibre.devices.prs505 import MEDIA_XML
|
from calibre.devices.prs505 import MEDIA_XML
|
||||||
@ -66,6 +65,41 @@ class PRS505(USBMS):
|
|||||||
def windows_filter_pnp_id(self, pnp_id):
|
def windows_filter_pnp_id(self, pnp_id):
|
||||||
return '_LAUNCHER' in pnp_id
|
return '_LAUNCHER' in pnp_id
|
||||||
|
|
||||||
|
def post_open_callback(self):
|
||||||
|
|
||||||
|
def write_cache(prefix):
|
||||||
|
try:
|
||||||
|
cachep = os.path.join(prefix, *(CACHE_XML.split('/')))
|
||||||
|
if not os.path.exists(cachep):
|
||||||
|
dname = os.path.dirname(cachep)
|
||||||
|
if not os.path.exists(dname):
|
||||||
|
try:
|
||||||
|
os.makedirs(dname, mode=0777)
|
||||||
|
except:
|
||||||
|
time.sleep(5)
|
||||||
|
os.makedirs(dname, mode=0777)
|
||||||
|
with open(cachep, 'wb') as f:
|
||||||
|
f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||||
|
</cache>
|
||||||
|
'''.encode('utf8'))
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Make sure we don't have the launcher partition
|
||||||
|
# as one of the cards
|
||||||
|
|
||||||
|
if self._card_a_prefix is not None:
|
||||||
|
if not write_cache(self._card_a_prefix):
|
||||||
|
self._card_a_prefix = None
|
||||||
|
if self._card_b_prefix is not None:
|
||||||
|
if not write_cache(self._card_b_prefix):
|
||||||
|
self._card_b_prefix = None
|
||||||
|
|
||||||
|
|
||||||
def get_device_information(self, end_session=True):
|
def get_device_information(self, end_session=True):
|
||||||
return (self.gui_name, '', '', '')
|
return (self.gui_name, '', '', '')
|
||||||
|
|
||||||
|
@ -415,10 +415,11 @@ class XMLCache(object):
|
|||||||
prints('\tmtime', strftime(os.path.getmtime(path)))
|
prints('\tmtime', strftime(os.path.getmtime(path)))
|
||||||
record.set('date', date)
|
record.set('date', date)
|
||||||
record.set('size', str(os.stat(path).st_size))
|
record.set('size', str(os.stat(path).st_size))
|
||||||
record.set('title', book.title)
|
title = book.title if book.title else _('Unknown')
|
||||||
|
record.set('title', title)
|
||||||
ts = book.title_sort
|
ts = book.title_sort
|
||||||
if not ts:
|
if not ts:
|
||||||
ts = title_sort(book.title)
|
ts = title_sort(title)
|
||||||
record.set('titleSorter', ts)
|
record.set('titleSorter', ts)
|
||||||
record.set('author', authors_to_string(book.authors))
|
record.set('author', authors_to_string(book.authors))
|
||||||
ext = os.path.splitext(path)[1]
|
ext = os.path.splitext(path)[1]
|
||||||
|
@ -44,6 +44,7 @@ def get_metadata_(src, encoding=None):
|
|||||||
author = match.group(2).replace(',', ';')
|
author = match.group(2).replace(',', ';')
|
||||||
|
|
||||||
ent_pat = re.compile(r'&(\S+)?;')
|
ent_pat = re.compile(r'&(\S+)?;')
|
||||||
|
if title:
|
||||||
title = ent_pat.sub(entity_to_unicode, title)
|
title = ent_pat.sub(entity_to_unicode, title)
|
||||||
if author:
|
if author:
|
||||||
author = ent_pat.sub(entity_to_unicode, author)
|
author = ent_pat.sub(entity_to_unicode, author)
|
||||||
|
@ -1334,7 +1334,7 @@ class MobiWriter(object):
|
|||||||
item = self._oeb.manifest.hrefs[href]
|
item = self._oeb.manifest.hrefs[href]
|
||||||
try:
|
try:
|
||||||
data = rescale_image(item.data, self._imagemax)
|
data = rescale_image(item.data, self._imagemax)
|
||||||
except IOError:
|
except:
|
||||||
self._oeb.logger.warn('Bad image file %r' % item.href)
|
self._oeb.logger.warn('Bad image file %r' % item.href)
|
||||||
continue
|
continue
|
||||||
self._records.append(data)
|
self._records.append(data)
|
||||||
|
@ -201,6 +201,11 @@ class CSSFlattener(object):
|
|||||||
tag = barename(node.tag)
|
tag = barename(node.tag)
|
||||||
style = stylizer.style(node)
|
style = stylizer.style(node)
|
||||||
cssdict = style.cssdict()
|
cssdict = style.cssdict()
|
||||||
|
try:
|
||||||
|
font_size = style['font-size']
|
||||||
|
except:
|
||||||
|
font_size = self.sbase if self.sbase is not None else \
|
||||||
|
self.context.source.fbase
|
||||||
if 'align' in node.attrib:
|
if 'align' in node.attrib:
|
||||||
cssdict['text-align'] = node.attrib['align']
|
cssdict['text-align'] = node.attrib['align']
|
||||||
del node.attrib['align']
|
del node.attrib['align']
|
||||||
@ -219,13 +224,16 @@ class CSSFlattener(object):
|
|||||||
esize = 1
|
esize = 1
|
||||||
if esize > 7:
|
if esize > 7:
|
||||||
esize = 7
|
esize = 7
|
||||||
cssdict['font-size'] = fnums[esize]
|
font_size = fnums[esize]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
cssdict['font-size'] = fnums[force_int(size)]
|
font_size = fnums[force_int(size)]
|
||||||
except:
|
except:
|
||||||
cssdict['font-size'] = fnums[3]
|
font_size = fnums[3]
|
||||||
|
cssdict['font-size'] = '%.1fpt'%font_size
|
||||||
del node.attrib['size']
|
del node.attrib['size']
|
||||||
|
if 'face' in node.attrib:
|
||||||
|
del node.attrib['face']
|
||||||
if 'color' in node.attrib:
|
if 'color' in node.attrib:
|
||||||
cssdict['color'] = node.attrib['color']
|
cssdict['color'] = node.attrib['color']
|
||||||
del node.attrib['color']
|
del node.attrib['color']
|
||||||
@ -244,7 +252,7 @@ class CSSFlattener(object):
|
|||||||
cssdict['font-size'] = '%0.5fem'%(fsize/psize)
|
cssdict['font-size'] = '%0.5fem'%(fsize/psize)
|
||||||
psize = fsize
|
psize = fsize
|
||||||
elif 'font-size' in cssdict or tag == 'body':
|
elif 'font-size' in cssdict or tag == 'body':
|
||||||
fsize = self.fmap[style['font-size']]
|
fsize = self.fmap[font_size]
|
||||||
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
||||||
psize = fsize
|
psize = fsize
|
||||||
if cssdict:
|
if cssdict:
|
||||||
|
@ -222,6 +222,8 @@ class DBAdder(Thread):
|
|||||||
|
|
||||||
class Adder(QObject):
|
class Adder(QObject):
|
||||||
|
|
||||||
|
ADD_TIMEOUT = 600 # seconds
|
||||||
|
|
||||||
def __init__(self, parent, db, callback, spare_server=None):
|
def __init__(self, parent, db, callback, spare_server=None):
|
||||||
QObject.__init__(self, parent)
|
QObject.__init__(self, parent)
|
||||||
self.pd = ProgressDialog(_('Adding...'), parent=parent)
|
self.pd = ProgressDialog(_('Adding...'), parent=parent)
|
||||||
@ -328,7 +330,7 @@ class Adder(QObject):
|
|||||||
except Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (time.time() - self.last_added_at) > 300:
|
if (time.time() - self.last_added_at) > self.ADD_TIMEOUT:
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
self.pd.hide()
|
self.pd.hide()
|
||||||
self.db_adder.end = True
|
self.db_adder.end = True
|
||||||
|
@ -445,6 +445,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.username.setText(opts.username)
|
self.username.setText(opts.username)
|
||||||
self.password.setText(opts.password if opts.password else '')
|
self.password.setText(opts.password if opts.password else '')
|
||||||
self.opt_max_opds_items.setValue(opts.max_opds_items)
|
self.opt_max_opds_items.setValue(opts.max_opds_items)
|
||||||
|
self.opt_max_opds_ungrouped_items.setValue(opts.max_opds_ungrouped_items)
|
||||||
self.auto_launch.setChecked(config['autolaunch_server'])
|
self.auto_launch.setChecked(config['autolaunch_server'])
|
||||||
self.systray_icon.setChecked(config['systray_icon'])
|
self.systray_icon.setChecked(config['systray_icon'])
|
||||||
self.sync_news.setChecked(config['upload_news_to_device'])
|
self.sync_news.setChecked(config['upload_news_to_device'])
|
||||||
@ -848,6 +849,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
sc.set('port', self.port.value())
|
sc.set('port', self.port.value())
|
||||||
sc.set('max_cover', mcs)
|
sc.set('max_cover', mcs)
|
||||||
sc.set('max_opds_items', self.opt_max_opds_items.value())
|
sc.set('max_opds_items', self.opt_max_opds_items.value())
|
||||||
|
sc.set('max_opds_ungrouped_items',
|
||||||
|
self.opt_max_opds_ungrouped_items.value())
|
||||||
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
||||||
config['upload_news_to_device'] = self.sync_news.isChecked()
|
config['upload_news_to_device'] = self.sync_news.isChecked()
|
||||||
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
||||||
|
@ -892,6 +892,26 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_max_opds_ungrouped_items">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>25</number>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>1000000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
|
<widget class="QLabel" name="label_16">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max. OPDS &ungrouped items:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_max_opds_ungrouped_items</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -3,14 +3,12 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, Qt
|
from PyQt4.QtCore import SIGNAL
|
||||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
from PyQt4.QtGui import QDialog
|
||||||
|
|
||||||
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
|
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
|
||||||
from calibre.utils.config import prefs
|
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.constants import islinux
|
|
||||||
|
|
||||||
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
from PyQt4.QtCore import SIGNAL, Qt
|
from PyQt4.QtCore import SIGNAL, Qt
|
||||||
from PyQt4.QtGui import QDialog, QListWidgetItem
|
from PyQt4.QtGui import QDialog, QListWidgetItem
|
||||||
|
|
||||||
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
||||||
from calibre.gui2 import question_dialog, error_dialog
|
from calibre.gui2 import question_dialog, error_dialog
|
||||||
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
|
||||||
class TagListEditor(QDialog, Ui_TagListEditor):
|
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||||
|
|
||||||
def tag_cmp(self, x, y):
|
def __init__(self, window, db, tag_to_match, category):
|
||||||
return cmp(x.lower(), y.lower())
|
|
||||||
|
|
||||||
def __init__(self, window, db, tag_to_match):
|
|
||||||
QDialog.__init__(self, window)
|
QDialog.__init__(self, window)
|
||||||
Ui_TagListEditor.__init__(self)
|
Ui_TagListEditor.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
@ -20,9 +20,28 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
self.to_delete = []
|
self.to_delete = []
|
||||||
self.db = db
|
self.db = db
|
||||||
self.all_tags = {}
|
self.all_tags = {}
|
||||||
for k,v in db.get_tags_with_ids():
|
self.category = category
|
||||||
|
if category == 'tags':
|
||||||
|
result = db.get_tags_with_ids()
|
||||||
|
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
|
elif category == 'series':
|
||||||
|
result = db.get_series_with_ids()
|
||||||
|
compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
|
||||||
|
elif category == 'publisher':
|
||||||
|
result = db.get_publishers_with_ids()
|
||||||
|
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
|
else: # should be a custom field
|
||||||
|
self.cc_label = None
|
||||||
|
if category in db.field_metadata:
|
||||||
|
self.cc_label = db.field_metadata[category]['label']
|
||||||
|
result = self.db.get_custom_items_with_ids(label=self.cc_label)
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
|
|
||||||
|
for k,v in result:
|
||||||
self.all_tags[v] = k
|
self.all_tags[v] = k
|
||||||
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
|
for tag in sorted(self.all_tags.keys(), cmp=compare):
|
||||||
item = QListWidgetItem(tag)
|
item = QListWidgetItem(tag)
|
||||||
item.setData(Qt.UserRole, self.all_tags[tag])
|
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||||
self.available_tags.addItem(item)
|
self.available_tags.addItem(item)
|
||||||
@ -37,13 +56,18 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
||||||
|
|
||||||
def finish_editing(self, item):
|
def finish_editing(self, item):
|
||||||
if item.text() != self.item_before_editing.text():
|
if not item.text():
|
||||||
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
error_dialog(self, _('Item is blank'),
|
||||||
error_dialog(self, 'Tag already used',
|
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||||
'The tag %s is already used.'%(item.text())).exec_()
|
|
||||||
item.setText(self.item_before_editing.text())
|
item.setText(self.item_before_editing.text())
|
||||||
return
|
return
|
||||||
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
|
if item.text() != self.item_before_editing.text():
|
||||||
|
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
||||||
|
error_dialog(self, _('Item already used'),
|
||||||
|
_('The item %s is already used.')%(item.text())).exec_()
|
||||||
|
item.setText(self.item_before_editing.text())
|
||||||
|
return
|
||||||
|
(id,ign) = self.item_before_editing.data(Qt.UserRole).toInt()
|
||||||
self.to_rename[item.text()] = id
|
self.to_rename[item.text()] = id
|
||||||
|
|
||||||
def rename_tag(self):
|
def rename_tag(self):
|
||||||
@ -52,38 +76,53 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
|
|
||||||
def _rename_tag(self, item):
|
def _rename_tag(self, item):
|
||||||
if item is None:
|
if item is None:
|
||||||
error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
|
error_dialog(self, _('No item selected'),
|
||||||
|
_('You must select one item from the list of Available items.')).exec_()
|
||||||
return
|
return
|
||||||
self.item_before_editing = item.clone()
|
self.item_before_editing = item.clone()
|
||||||
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
||||||
self.available_tags.editItem(item)
|
self.available_tags.editItem(item)
|
||||||
|
|
||||||
def delete_tags(self, item=None):
|
def delete_tags(self, item=None):
|
||||||
confirms, deletes = [], []
|
deletes = self.available_tags.selectedItems() if item is None else [item]
|
||||||
items = self.available_tags.selectedItems() if item is None else [item]
|
if not deletes:
|
||||||
if not items:
|
error_dialog(self, _('No items selected'),
|
||||||
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
|
_('You must select at least one items from the list.')).exec_()
|
||||||
|
return
|
||||||
|
ct = ', '.join([unicode(item.text()) for item in deletes])
|
||||||
|
if not question_dialog(self, _('Are your sure?'),
|
||||||
|
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
|
||||||
return
|
return
|
||||||
for item in items:
|
|
||||||
if self.db.is_tag_used(unicode(item.text())):
|
|
||||||
confirms.append(item)
|
|
||||||
else:
|
|
||||||
deletes.append(item)
|
|
||||||
if confirms:
|
|
||||||
ct = ', '.join([unicode(item.text()) for item in confirms])
|
|
||||||
if question_dialog(self, _('Are your sure?'),
|
|
||||||
'<p>'+_('The following tags are used by one or more books. '
|
|
||||||
'Are you certain you want to delete them?')+'<br>'+ct):
|
|
||||||
deletes += confirms
|
|
||||||
|
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
self.to_delete.append(item)
|
(id,ign) = item.data(Qt.UserRole).toInt()
|
||||||
|
self.to_delete.append(id)
|
||||||
self.available_tags.takeItem(self.available_tags.row(item))
|
self.available_tags.takeItem(self.available_tags.row(item))
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
for text in self.to_rename:
|
rename_func = None
|
||||||
self.db.rename_tag(self.to_rename[text], unicode(text))
|
if self.category == 'tags':
|
||||||
for item in self.to_delete:
|
rename_func = self.db.rename_tag
|
||||||
self.db.delete_tag(unicode(item.text()))
|
delete_func = self.db.delete_tag_using_id
|
||||||
QDialog.accept(self)
|
elif self.category == 'series':
|
||||||
|
rename_func = self.db.rename_series
|
||||||
|
delete_func = self.db.delete_series_using_id
|
||||||
|
elif self.category == 'publisher':
|
||||||
|
rename_func = self.db.rename_publisher
|
||||||
|
delete_func = self.db.delete_publisher_using_id
|
||||||
|
else:
|
||||||
|
rename_func = partial(self.db.rename_custom_item, label=self.cc_label)
|
||||||
|
delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label)
|
||||||
|
|
||||||
|
work_done = False
|
||||||
|
if rename_func:
|
||||||
|
for text in self.to_rename:
|
||||||
|
work_done = True
|
||||||
|
rename_func(id=self.to_rename[text], new_name=unicode(text))
|
||||||
|
for item in self.to_delete:
|
||||||
|
work_done = True
|
||||||
|
delete_func(item)
|
||||||
|
if not work_done:
|
||||||
|
QDialog.reject(self)
|
||||||
|
else:
|
||||||
|
QDialog.accept(self)
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Tag Editor</string>
|
<string>Category Editor</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Tags in use</string>
|
<string>Items in use</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="buddy">
|
<property name="buddy">
|
||||||
<cstring>available_tags</cstring>
|
<cstring>available_tags</cstring>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="delete_button">
|
<widget class="QToolButton" name="delete_button">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
|
<string>Delete item from database. This will unapply the item from all books and then remove it from the database.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="rename_button">
|
<widget class="QToolButton" name="rename_button">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Rename the tag everywhere it is used.</string>
|
<string>Rename the item in every book where it is used.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
|
@ -213,7 +213,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
self.count_changed()
|
self.count_changed()
|
||||||
|
|
||||||
def search(self, text, refinement, reset=True):
|
def search(self, text, reset=True):
|
||||||
try:
|
try:
|
||||||
self.db.search(text)
|
self.db.search(text)
|
||||||
except ParseException:
|
except ParseException:
|
||||||
@ -224,9 +224,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
if self.last_search:
|
if self.last_search:
|
||||||
|
# Do not issue search done for the null search. It is used to clear
|
||||||
|
# the search and count records for restrictions
|
||||||
self.searched.emit(True)
|
self.searched.emit(True)
|
||||||
|
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
if not self.db:
|
if not self.db:
|
||||||
return
|
return
|
||||||
@ -257,7 +258,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.sort(col, self.sorted_on[1], reset=reset)
|
self.sort(col, self.sorted_on[1], reset=reset)
|
||||||
|
|
||||||
def research(self, reset=True):
|
def research(self, reset=True):
|
||||||
self.search(self.last_search, False, reset=reset)
|
self.search(self.last_search, reset=reset)
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
if parent and parent.isValid():
|
if parent and parent.isValid():
|
||||||
@ -730,6 +731,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.db.data.set_search_restriction(s)
|
self.db.data.set_search_restriction(s)
|
||||||
|
self.search('')
|
||||||
|
return self.rowCount(None)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -874,7 +877,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
|
||||||
def search(self, text, refinement, reset=True):
|
def search(self, text, reset=True):
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
self.map = list(range(len(self.db)))
|
self.map = list(range(len(self.db)))
|
||||||
else:
|
else:
|
||||||
@ -1086,7 +1089,6 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
idx = self.map[row]
|
idx = self.map[row]
|
||||||
if cname == 'title' :
|
if cname == 'title' :
|
||||||
self.db[idx].title = val
|
self.db[idx].title = val
|
||||||
self.db[idx].title_sorter = val
|
|
||||||
elif cname == 'authors':
|
elif cname == 'authors':
|
||||||
self.db[idx].authors = string_to_authors(val)
|
self.db[idx].authors = string_to_authors(val)
|
||||||
elif cname == 'collections':
|
elif cname == 'collections':
|
||||||
|
@ -75,6 +75,9 @@ class BooksView(QTableView): # {{{
|
|||||||
h.setSectionHidden(idx, True)
|
h.setSectionHidden(idx, True)
|
||||||
elif action == 'show':
|
elif action == 'show':
|
||||||
h.setSectionHidden(idx, False)
|
h.setSectionHidden(idx, False)
|
||||||
|
if h.sectionSize(idx) < 3:
|
||||||
|
sz = h.sectionSizeHint(idx)
|
||||||
|
h.resizeSection(idx, sz)
|
||||||
elif action == 'ascending':
|
elif action == 'ascending':
|
||||||
self.sortByColumn(idx, Qt.AscendingOrder)
|
self.sortByColumn(idx, Qt.AscendingOrder)
|
||||||
elif action == 'descending':
|
elif action == 'descending':
|
||||||
@ -257,6 +260,11 @@ class BooksView(QTableView): # {{{
|
|||||||
for col, alignment in state.get('column_alignment', {}).items():
|
for col, alignment in state.get('column_alignment', {}).items():
|
||||||
self._model.change_alignment(col, alignment)
|
self._model.change_alignment(col, alignment)
|
||||||
|
|
||||||
|
for i in range(h.count()):
|
||||||
|
if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
|
||||||
|
sz = h.sectionSizeHint(i)
|
||||||
|
h.resizeSection(i, sz)
|
||||||
|
|
||||||
def get_default_state(self):
|
def get_default_state(self):
|
||||||
old_state = {
|
old_state = {
|
||||||
'hidden_columns': [],
|
'hidden_columns': [],
|
||||||
@ -429,10 +437,6 @@ class BooksView(QTableView): # {{{
|
|||||||
self._search_done = search_done
|
self._search_done = search_done
|
||||||
self._model.searched.connect(self.search_done)
|
self._model.searched.connect(self.search_done)
|
||||||
|
|
||||||
def connect_to_restriction_set(self, tv):
|
|
||||||
# must be synchronous (not queued)
|
|
||||||
tv.restriction_set.connect(self._model.set_search_restriction)
|
|
||||||
|
|
||||||
def connect_to_book_display(self, bd):
|
def connect_to_book_display(self, bd):
|
||||||
self._model.new_bookdisplay_data.connect(bd)
|
self._model.new_bookdisplay_data.connect(bd)
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.stack.setCurrentIndex(1)
|
self.stack.setCurrentIndex(1)
|
||||||
self.renderer.start()
|
self.renderer.start()
|
||||||
|
|
||||||
def find(self, search, refinement):
|
def find(self, search):
|
||||||
self.last_search = search
|
self.last_search = search
|
||||||
try:
|
try:
|
||||||
self.document.search(search)
|
self.document.search(search)
|
||||||
|
@ -226,7 +226,7 @@ class GuiRunner(QObject):
|
|||||||
self.splash_pixmap = QPixmap()
|
self.splash_pixmap = QPixmap()
|
||||||
self.splash_pixmap.load(I('library.png'))
|
self.splash_pixmap.load(I('library.png'))
|
||||||
self.splash_screen = QSplashScreen(self.splash_pixmap,
|
self.splash_screen = QSplashScreen(self.splash_pixmap,
|
||||||
Qt.SplashScreen|Qt.WindowStaysOnTopHint)
|
Qt.SplashScreen)
|
||||||
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
|
self.splash_screen.showMessage(_('Starting %s: Loading books...') %
|
||||||
__appname__)
|
__appname__)
|
||||||
self.splash_screen.show()
|
self.splash_screen.show()
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="centralwidget">
|
<widget class="QWidget" name="centralwidget">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
@ -304,12 +304,10 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="Splitter" name="vertical_splitter">
|
<widget class="Splitter" name="vertical_splitter">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>100</verstretch>
|
<verstretch>100</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
@ -317,6 +315,9 @@
|
|||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<widget class="QWidget" name="layoutWidget">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
<widget class="QStackedWidget" name="stack">
|
<widget class="QStackedWidget" name="stack">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
@ -328,7 +329,7 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="library">
|
<widget class="QWidget" name="library">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="Splitter" name="horizontal_splitter">
|
<widget class="Splitter" name="horizontal_splitter">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
@ -539,8 +540,6 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="StatusBar" name="status_bar" native="true"/>
|
|
||||||
</widget>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="SideBar" name="sidebar" native="true">
|
<widget class="SideBar" name="sidebar" native="true">
|
||||||
@ -550,9 +549,18 @@
|
|||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>30</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="StatusBar" name="status_bar" native="true"/>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -57,7 +57,7 @@ class SearchBox2(QComboBox):
|
|||||||
INTERVAL = 1500 #: Time to wait before emitting search signal
|
INTERVAL = 1500 #: Time to wait before emitting search signal
|
||||||
MAX_COUNT = 25
|
MAX_COUNT = 25
|
||||||
|
|
||||||
search = pyqtSignal(object, object)
|
search = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QComboBox.__init__(self, parent)
|
QComboBox.__init__(self, parent)
|
||||||
@ -97,8 +97,12 @@ class SearchBox2(QComboBox):
|
|||||||
self.help_state = False
|
self.help_state = False
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
|
self.search.emit('')
|
||||||
self._in_a_search = False
|
self._in_a_search = False
|
||||||
self.setEditText(self.help_text)
|
self.setEditText(self.help_text)
|
||||||
|
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||||
|
self.killTimer(self.timer)
|
||||||
|
self.timer = None
|
||||||
self.line_edit.home(False)
|
self.line_edit.home(False)
|
||||||
self.help_state = True
|
self.help_state = True
|
||||||
self.line_edit.setStyleSheet(
|
self.line_edit.setStyleSheet(
|
||||||
@ -111,7 +115,6 @@ class SearchBox2(QComboBox):
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
self.search.emit('', False)
|
|
||||||
|
|
||||||
def search_done(self, ok):
|
def search_done(self, ok):
|
||||||
if not unicode(self.currentText()).strip():
|
if not unicode(self.currentText()).strip():
|
||||||
@ -155,9 +158,8 @@ class SearchBox2(QComboBox):
|
|||||||
if not text or text == self.help_text:
|
if not text or text == self.help_text:
|
||||||
return self.clear()
|
return self.clear()
|
||||||
self.help_state = False
|
self.help_state = False
|
||||||
refinement = text.startswith(self.prev_search) and ':' not in text
|
|
||||||
self.prev_search = text
|
self.prev_search = text
|
||||||
self.search.emit(text, refinement)
|
self.search.emit(text)
|
||||||
|
|
||||||
idx = self.findText(text, Qt.MatchFixedString)
|
idx = self.findText(text, Qt.MatchFixedString)
|
||||||
self.block_signals(True)
|
self.block_signals(True)
|
||||||
@ -187,12 +189,15 @@ class SearchBox2(QComboBox):
|
|||||||
self.set_search_string(joiner.join(tags))
|
self.set_search_string(joiner.join(tags))
|
||||||
|
|
||||||
def set_search_string(self, txt):
|
def set_search_string(self, txt):
|
||||||
|
if not txt:
|
||||||
|
self.clear_to_help()
|
||||||
|
return
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
self.setEditText(txt)
|
self.setEditText(txt)
|
||||||
if self.timer is not None: # Turn off any timers that got started in setEditText
|
if self.timer is not None: # Turn off any timers that got started in setEditText
|
||||||
self.killTimer(self.timer)
|
self.killTimer(self.timer)
|
||||||
self.timer = None
|
self.timer = None
|
||||||
self.search.emit(txt, False)
|
self.search.emit(txt)
|
||||||
self.line_edit.end(False)
|
self.line_edit.end(False)
|
||||||
self.initial_state = False
|
self.initial_state = False
|
||||||
|
|
||||||
|
@ -17,15 +17,17 @@ from calibre.gui2 import config, NONE
|
|||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.library.field_metadata import TagsIcons
|
from calibre.library.field_metadata import TagsIcons
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
|
||||||
class TagsView(QTreeView): # {{{
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
need_refresh = pyqtSignal()
|
refresh_required = pyqtSignal()
|
||||||
restriction_set = pyqtSignal(object)
|
|
||||||
tags_marked = pyqtSignal(object, object)
|
tags_marked = pyqtSignal(object, object)
|
||||||
user_category_edit = pyqtSignal(object)
|
user_category_edit = pyqtSignal(object)
|
||||||
tag_list_edit = pyqtSignal(object)
|
tag_list_edit = pyqtSignal(object, object)
|
||||||
saved_search_edit = pyqtSignal(object)
|
saved_search_edit = pyqtSignal(object)
|
||||||
|
tag_item_renamed = pyqtSignal()
|
||||||
|
search_item_renamed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QTreeView.__init__(self, *args)
|
QTreeView.__init__(self, *args)
|
||||||
@ -34,26 +36,26 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setIconSize(QSize(30, 30))
|
self.setIconSize(QSize(30, 30))
|
||||||
self.tag_match = None
|
self.tag_match = None
|
||||||
|
|
||||||
def set_database(self, db, tag_match, popularity, restriction):
|
def set_database(self, db, tag_match, popularity):
|
||||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
|
self._model = TagsModel(db, parent=self,
|
||||||
|
hidden_categories=self.hidden_categories,
|
||||||
|
search_restriction=None)
|
||||||
self.popularity = popularity
|
self.popularity = popularity
|
||||||
self.restriction = restriction
|
|
||||||
self.tag_match = tag_match
|
self.tag_match = tag_match
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.search_restriction = None
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.clicked.connect(self.toggle)
|
self.clicked.connect(self.toggle)
|
||||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
self.popularity.setChecked(config['sort_by_popularity'])
|
||||||
self.popularity.stateChanged.connect(self.sort_changed)
|
self.popularity.stateChanged.connect(self.sort_changed)
|
||||||
self.restriction.activated[str].connect(self.search_restriction_set)
|
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
||||||
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
|
|
||||||
db.add_listener(self.database_changed)
|
db.add_listener(self.database_changed)
|
||||||
self.saved_searches_changed(recount=False)
|
|
||||||
|
|
||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.need_refresh.emit()
|
self.refresh_required.emit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def match_all(self):
|
def match_all(self):
|
||||||
@ -64,34 +66,38 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.model().refresh()
|
self.model().refresh()
|
||||||
# self.search_restriction_set()
|
# self.search_restriction_set()
|
||||||
|
|
||||||
def search_restriction_set(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.clear()
|
if s:
|
||||||
if len(s) == 0:
|
self.search_restriction = s
|
||||||
self.search_restriction = ''
|
|
||||||
else:
|
else:
|
||||||
self.search_restriction = 'search:"%s"' % unicode(s).strip()
|
self.search_restriction = None
|
||||||
self.model().set_search_restriction(self.search_restriction)
|
self.set_new_model()
|
||||||
self.restriction_set.emit(self.search_restriction)
|
|
||||||
self.recount() # Must happen after the emission of the restriction_set signal
|
|
||||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
# Swallow everything except leftButton so context menus work correctly
|
# Swallow everything except leftButton so context menus work correctly
|
||||||
if event.button() == Qt.LeftButton:
|
if event.button() == Qt.LeftButton:
|
||||||
QTreeView.mouseReleaseEvent(self, event)
|
QTreeView.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event):
|
||||||
|
# swallow these to avoid toggling and editing at the same time
|
||||||
|
pass
|
||||||
|
|
||||||
def toggle(self, index):
|
def toggle(self, index):
|
||||||
modifiers = int(QApplication.keyboardModifiers())
|
modifiers = int(QApplication.keyboardModifiers())
|
||||||
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
||||||
if self._model.toggle(index, exclusive):
|
if self._model.toggle(index, exclusive):
|
||||||
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
self.tags_marked.emit(self._model.tokens(), self.match_all)
|
||||||
|
|
||||||
def context_menu_handler(self, action=None, category=None):
|
def context_menu_handler(self, action=None, category=None,
|
||||||
|
key=None, index=None):
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if action == 'manage_tags':
|
if action == 'edit_item':
|
||||||
self.tag_list_edit.emit(category)
|
self.edit(index)
|
||||||
|
return
|
||||||
|
if action == 'open_editor':
|
||||||
|
self.tag_list_edit.emit(category, key)
|
||||||
return
|
return
|
||||||
if action == 'manage_categories':
|
if action == 'manage_categories':
|
||||||
self.user_category_edit.emit(category)
|
self.user_category_edit.emit(category)
|
||||||
@ -117,29 +123,51 @@ class TagsView(QTreeView): # {{{
|
|||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
tag_name = ''
|
tag_name = ''
|
||||||
if item.type == TagTreeItem.TAG:
|
if item.type == TagTreeItem.TAG:
|
||||||
|
tag_item = item
|
||||||
tag_name = item.tag.name
|
tag_name = item.tag.name
|
||||||
item = item.parent
|
item = item.parent
|
||||||
if item.type == TagTreeItem.CATEGORY:
|
if item.type == TagTreeItem.CATEGORY:
|
||||||
category = unicode(item.name.toString())
|
category = unicode(item.name.toString())
|
||||||
self.context_menu = QMenu(self)
|
key = item.category_key
|
||||||
self.context_menu.addAction(_('Hide %s') % category,
|
# Verify that we are working with a field that we know something about
|
||||||
partial(self.context_menu_handler, action='hide', category=category))
|
if key not in self.db.field_metadata:
|
||||||
|
return True
|
||||||
|
|
||||||
if self.hidden_categories:
|
self.context_menu = QMenu(self)
|
||||||
|
# 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']):
|
||||||
|
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
|
||||||
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
|
category=tag_item, index=index))
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
|
# Hide/Show/Restore categories
|
||||||
|
self.context_menu.addAction(_('Hide category %s') % category,
|
||||||
|
partial(self.context_menu_handler, action='hide', category=category))
|
||||||
|
if self.hidden_categories:
|
||||||
m = self.context_menu.addMenu(_('Show category'))
|
m = self.context_menu.addMenu(_('Show category'))
|
||||||
for col in self.hidden_categories:
|
for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())):
|
||||||
m.addAction(col,
|
m.addAction(col,
|
||||||
partial(self.context_menu_handler, action='show', category=col))
|
partial(self.context_menu_handler, action='show', category=col))
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addAction(_('Show all categories'),
|
||||||
self.context_menu.addAction(_('Restore defaults'),
|
|
||||||
partial(self.context_menu_handler, action='defaults'))
|
partial(self.context_menu_handler, action='defaults'))
|
||||||
|
|
||||||
|
# Offer specific editors for tags/series/publishers/saved searches
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
self.context_menu.addAction(_('Manage Tags'),
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
partial(self.context_menu_handler, action='manage_tags',
|
self.db.field_metadata[key]['is_custom']:
|
||||||
|
self.context_menu.addAction(_('Manage ') + category,
|
||||||
|
partial(self.context_menu_handler, action='open_editor',
|
||||||
|
category=tag_name, key=key))
|
||||||
|
elif key == 'search':
|
||||||
|
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||||
|
partial(self.context_menu_handler, action='manage_searches',
|
||||||
category=tag_name))
|
category=tag_name))
|
||||||
|
|
||||||
|
# Always show the user categories editor
|
||||||
|
self.context_menu.addSeparator()
|
||||||
if category in prefs['user_categories'].keys():
|
if category in prefs['user_categories'].keys():
|
||||||
self.context_menu.addAction(_('Manage User Categories'),
|
self.context_menu.addAction(_('Manage User Categories'),
|
||||||
partial(self.context_menu_handler, action='manage_categories',
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
@ -149,30 +177,13 @@ class TagsView(QTreeView): # {{{
|
|||||||
partial(self.context_menu_handler, action='manage_categories',
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
category=None))
|
category=None))
|
||||||
|
|
||||||
self.context_menu.addAction(_('Manage Saved Searches'),
|
|
||||||
partial(self.context_menu_handler, action='manage_searches',
|
|
||||||
category=tag_name))
|
|
||||||
|
|
||||||
self.context_menu.popup(self.mapToGlobal(point))
|
self.context_menu.popup(self.mapToGlobal(point))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
if self.model():
|
||||||
self.model().clear_state()
|
self.model().clear_state()
|
||||||
|
|
||||||
def saved_searches_changed(self, recount=True):
|
|
||||||
p = prefs['saved_searches'].keys()
|
|
||||||
p.sort()
|
|
||||||
t = self.restriction.currentText()
|
|
||||||
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
|
|
||||||
self.restriction.addItem('')
|
|
||||||
for s in p:
|
|
||||||
self.restriction.addItem(s)
|
|
||||||
if t in p: # redo the current restriction, if there was one
|
|
||||||
self.restriction.setCurrentIndex(self.restriction.findText(t))
|
|
||||||
self.search_restriction_set(t)
|
|
||||||
if recount:
|
|
||||||
self.recount()
|
|
||||||
|
|
||||||
def recount(self, *args):
|
def recount(self, *args):
|
||||||
ci = self.currentIndex()
|
ci = self.currentIndex()
|
||||||
if not ci.isValid():
|
if not ci.isValid():
|
||||||
@ -193,7 +204,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
# model. Reason: it is much easier than reconstructing the browser tree.
|
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||||
def set_new_model(self):
|
def set_new_model(self):
|
||||||
self._model = TagsModel(self.db, parent=self,
|
self._model = TagsModel(self.db, parent=self,
|
||||||
hidden_categories=self.hidden_categories)
|
hidden_categories=self.hidden_categories,
|
||||||
|
search_restriction=self.search_restriction)
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -203,7 +215,8 @@ class TagTreeItem(object): # {{{
|
|||||||
TAG = 1
|
TAG = 1
|
||||||
ROOT = 2
|
ROOT = 2
|
||||||
|
|
||||||
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None):
|
def __init__(self, data=None, category_icon=None, icon_map=None,
|
||||||
|
parent=None, tooltip=None, category_key=None):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.children = []
|
self.children = []
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
@ -218,6 +231,7 @@ class TagTreeItem(object): # {{{
|
|||||||
self.bold_font = QFont()
|
self.bold_font = QFont()
|
||||||
self.bold_font.setBold(True)
|
self.bold_font.setBold(True)
|
||||||
self.bold_font = QVariant(self.bold_font)
|
self.bold_font = QVariant(self.bold_font)
|
||||||
|
self.category_key = category_key
|
||||||
elif self.type == self.TAG:
|
elif self.type == self.TAG:
|
||||||
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))
|
||||||
@ -263,6 +277,8 @@ class TagTreeItem(object): # {{{
|
|||||||
return QVariant('%s'%(self.tag.name))
|
return QVariant('%s'%(self.tag.name))
|
||||||
else:
|
else:
|
||||||
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
||||||
|
if role == Qt.EditRole:
|
||||||
|
return QVariant(self.tag.name)
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon_state_map[self.tag.state]
|
return self.icon_state_map[self.tag.state]
|
||||||
if role == Qt.ToolTipRole and self.tag.tooltip is not None:
|
if role == Qt.ToolTipRole and self.tag.tooltip is not None:
|
||||||
@ -277,7 +293,7 @@ class TagTreeItem(object): # {{{
|
|||||||
|
|
||||||
class TagsModel(QAbstractItemModel): # {{{
|
class TagsModel(QAbstractItemModel): # {{{
|
||||||
|
|
||||||
def __init__(self, db, parent=None, hidden_categories=None):
|
def __init__(self, db, parent, hidden_categories=None, search_restriction=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
|
|
||||||
# must do this here because 'QPixmap: Must construct a QApplication
|
# must do this here because 'QPixmap: Must construct a QApplication
|
||||||
@ -297,9 +313,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.tags_view = parent
|
||||||
self.hidden_categories = hidden_categories
|
self.hidden_categories = hidden_categories
|
||||||
self.search_restriction = ''
|
self.search_restriction = search_restriction
|
||||||
self.ignore_next_search = 0
|
|
||||||
|
|
||||||
# Reconstruct the user categories, putting them into metadata
|
# Reconstruct the user categories, putting them into metadata
|
||||||
tb_cats = self.db.field_metadata
|
tb_cats = self.db.field_metadata
|
||||||
@ -324,7 +340,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
c = TagTreeItem(parent=self.root_item,
|
c = 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],
|
||||||
tooltip=tt)
|
tooltip=tt, category_key=r)
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||||
|
|
||||||
@ -335,18 +351,22 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.row_map = []
|
self.row_map = []
|
||||||
self.categories = []
|
self.categories = []
|
||||||
|
|
||||||
if len(self.search_restriction):
|
if self.search_restriction:
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map,
|
data = self.db.get_categories(sort_on_count=sort,
|
||||||
ids=self.db.search(self.search_restriction, return_matches=True))
|
icon_map=self.category_icon_map,
|
||||||
|
ids=self.db.search('', return_matches=True))
|
||||||
else:
|
else:
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
||||||
|
|
||||||
tb_categories = self.db.field_metadata
|
tb_categories = self.db.field_metadata
|
||||||
|
self.category_items = {}
|
||||||
for category in tb_categories:
|
for category in tb_categories:
|
||||||
if category in data: # They should always be there, but ...
|
if category in data: # They should always be there, but ...
|
||||||
|
# make a map of sets of names per category for duplicate
|
||||||
|
# checking when editing
|
||||||
|
self.category_items[category] = set([tag.name for tag in data[category]])
|
||||||
self.row_map.append(category)
|
self.row_map.append(category)
|
||||||
self.categories.append(tb_categories[category]['name'])
|
self.categories.append(tb_categories[category]['name'])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
@ -382,11 +402,52 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
return item.data(role)
|
return item.data(role)
|
||||||
|
|
||||||
|
def setData(self, index, value, role=Qt.EditRole):
|
||||||
|
if not index.isValid():
|
||||||
|
return NONE
|
||||||
|
val = unicode(value.toString())
|
||||||
|
if not val:
|
||||||
|
error_dialog(self.tags_view, _('Item is blank'),
|
||||||
|
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||||
|
return False
|
||||||
|
item = index.internalPointer()
|
||||||
|
key = item.parent.category_key
|
||||||
|
# make certain we know about the category
|
||||||
|
if key not in self.db.field_metadata:
|
||||||
|
return
|
||||||
|
if val in self.category_items[key]:
|
||||||
|
error_dialog(self.tags_view, 'Duplicate item',
|
||||||
|
_('The name %s is already used.')%val).exec_()
|
||||||
|
return False
|
||||||
|
oldval = item.tag.name
|
||||||
|
if key == 'search':
|
||||||
|
saved_searches.rename(unicode(item.data(role).toString()), val)
|
||||||
|
self.tags_view.search_item_renamed.emit()
|
||||||
|
else:
|
||||||
|
if key == 'series':
|
||||||
|
self.db.rename_series(item.tag.id, val)
|
||||||
|
elif key == 'publisher':
|
||||||
|
self.db.rename_publisher(item.tag.id, val)
|
||||||
|
elif key == 'tags':
|
||||||
|
self.db.rename_tag(item.tag.id, val)
|
||||||
|
elif key == 'authors':
|
||||||
|
self.db.rename_author(item.tag.id, val)
|
||||||
|
elif self.db.field_metadata[key]['is_custom']:
|
||||||
|
self.db.rename_custom_item(item.tag.id, val,
|
||||||
|
label=self.db.field_metadata[key]['label'])
|
||||||
|
self.tags_view.tag_item_renamed.emit()
|
||||||
|
item.tag.name = val
|
||||||
|
self.dataChanged.emit(index, index)
|
||||||
|
# replace the old value in the duplicate detection map with the new one
|
||||||
|
self.category_items[key].discard(oldval)
|
||||||
|
self.category_items[key].add(val)
|
||||||
|
return True
|
||||||
|
|
||||||
def headerData(self, *args):
|
def headerData(self, *args):
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def flags(self, *args):
|
def flags(self, *args):
|
||||||
return Qt.ItemIsEnabled|Qt.ItemIsSelectable
|
return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
|
||||||
|
|
||||||
def path_for_index(self, index):
|
def path_for_index(self, index):
|
||||||
ans = []
|
ans = []
|
||||||
@ -464,12 +525,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def clear_state(self):
|
def clear_state(self):
|
||||||
self.reset_all_states()
|
self.reset_all_states()
|
||||||
|
|
||||||
def reinit(self, *args, **kwargs):
|
|
||||||
if self.ignore_next_search == 0:
|
|
||||||
self.reset_all_states()
|
|
||||||
else:
|
|
||||||
self.ignore_next_search -= 1
|
|
||||||
|
|
||||||
def toggle(self, index, exclusive):
|
def toggle(self, index, exclusive):
|
||||||
if not index.isValid(): return False
|
if not index.isValid(): return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
@ -477,7 +532,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
item.toggle()
|
item.toggle()
|
||||||
if exclusive:
|
if exclusive:
|
||||||
self.reset_all_states(except_=item.tag)
|
self.reset_all_states(except_=item.tag)
|
||||||
self.ignore_next_search = 2
|
|
||||||
self.dataChanged.emit(index, index)
|
self.dataChanged.emit(index, index)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -160,9 +160,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.restriction_in_effect = False
|
self.restriction_in_effect = False
|
||||||
self.search.initialize('main_search_history', colorize=True,
|
self.search.initialize('main_search_history', colorize=True,
|
||||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
|
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
|
||||||
self.search_clear()
|
self.search.clear()
|
||||||
|
|
||||||
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
self.saved_search.initialize(saved_searches, self.search, colorize=True,
|
||||||
help_text=_('Saved Searches'))
|
help_text=_('Saved Searches'))
|
||||||
@ -521,8 +521,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_done)),
|
self.search_done)),
|
||||||
('connect_to_book_display',
|
('connect_to_book_display',
|
||||||
(self.status_bar.book_info.show_data,)),
|
(self.status_bar.book_info.show_data,)),
|
||||||
('connect_to_restriction_set',
|
|
||||||
(self.tags_view,)),
|
|
||||||
]:
|
]:
|
||||||
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
||||||
getattr(view, func)(*args)
|
getattr(view, func)(*args)
|
||||||
@ -545,22 +543,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.cover_cache.start()
|
self.cover_cache.start()
|
||||||
self.library_view.model().cover_cache = self.cover_cache
|
self.library_view.model().cover_cache = self.cover_cache
|
||||||
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
|
||||||
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
self.search_restriction.activated[str].connect(self.apply_search_restriction)
|
||||||
|
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
||||||
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
self.tags_view.tags_marked.connect(self.search.search_from_tags)
|
||||||
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
|
||||||
self.tags_view.restriction_set.connect(x)
|
|
||||||
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
||||||
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||||
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||||
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||||
self.search.search.connect(self.tags_view.model().reinit)
|
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||||
|
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
||||||
for x in (self.location_view.count_changed, self.tags_view.recount,
|
for x in (self.location_view.count_changed, self.tags_view.recount,
|
||||||
self.restriction_count_changed):
|
self.restriction_count_changed):
|
||||||
self.library_view.model().count_changed_signal.connect(x)
|
self.library_view.model().count_changed_signal.connect(x)
|
||||||
|
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||||
self.connect(self.saved_search, SIGNAL('changed()'),
|
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
|
||||||
self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
self.saved_searches_changed()
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
@ -583,7 +581,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.search_restriction.setMinimumContentsLength(10)
|
self.search_restriction.setMinimumContentsLength(10)
|
||||||
|
|
||||||
|
|
||||||
########################### Cover Flow ################################
|
########################### Cover Flow ################################
|
||||||
self.cover_flow = None
|
self.cover_flow = None
|
||||||
if CoverFlow is not None:
|
if CoverFlow is not None:
|
||||||
@ -623,7 +620,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.sidebar.job_done, Qt.QueuedConnection)
|
self.sidebar.job_done, Qt.QueuedConnection)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if config['autolaunch_server']:
|
if config['autolaunch_server']:
|
||||||
from calibre.library.server.main import start_threaded_server
|
from calibre.library.server.main import start_threaded_server
|
||||||
from calibre.library.server import server_config
|
from calibre.library.server import server_config
|
||||||
@ -660,19 +656,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.tags_view.set_new_model()
|
self.tags_view.set_new_model()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
def do_tags_list_edit(self, tag):
|
def do_tags_list_edit(self, tag, category):
|
||||||
d = TagListEditor(self, self.library_view.model().db, tag)
|
d = TagListEditor(self, self.library_view.model().db, tag, category)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == d.Accepted:
|
if d.result() == d.Accepted:
|
||||||
|
# Clean up everything, as information could have changed for many books.
|
||||||
|
self.library_view.model().refresh()
|
||||||
self.tags_view.set_new_model()
|
self.tags_view.set_new_model()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
self.saved_search.clear_to_help()
|
||||||
|
self.search.clear_to_help()
|
||||||
|
|
||||||
|
def do_tag_item_renamed(self):
|
||||||
|
# Clean up library view and search
|
||||||
self.library_view.model().refresh()
|
self.library_view.model().refresh()
|
||||||
|
self.saved_search.clear_to_help()
|
||||||
|
self.search.clear_to_help()
|
||||||
|
|
||||||
def do_saved_search_edit(self, search):
|
def do_saved_search_edit(self, search):
|
||||||
d = SavedSearchEditor(self, search)
|
d = SavedSearchEditor(self, search)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == d.Accepted:
|
if d.result() == d.Accepted:
|
||||||
self.tags_view.saved_searches_changed(recount=True)
|
self.saved_searches_changed()
|
||||||
self.saved_search.clear_to_help()
|
self.saved_search.clear_to_help()
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
@ -831,19 +836,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
sm.select(idx, sm.ClearAndSelect|sm.Rows)
|
sm.select(idx, sm.ClearAndSelect|sm.Rows)
|
||||||
self.library_view.setCurrentIndex(idx)
|
self.library_view.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Handling of the count of books in a restricted view requires that
|
Restrictions.
|
||||||
we capture the count after the initial restriction search. To so this,
|
Adding and deleting books creates a complexity. When added, they are
|
||||||
we require that the restriction_set signal be issued before the search signal,
|
displayed regardless of whether they match a search restriction. However, if
|
||||||
so that when the search_done happens and the count is displayed,
|
they do not, they are removed at the next search. The counts must take this
|
||||||
we can grab the count. This works because the search box is cleared
|
|
||||||
when a restriction is set, so that first search will find all books.
|
|
||||||
|
|
||||||
Adding and deleting books creates another complexity. When added, they are
|
|
||||||
displayed regardless of whether they match the restriction. However, if they
|
|
||||||
do not, they are removed at the next search. The counts must take this
|
|
||||||
behavior into effect.
|
behavior into effect.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@ -851,15 +848,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
|
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
|
||||||
self.restriction_count_of_books_in_library = c
|
self.restriction_count_of_books_in_library = c
|
||||||
if self.restriction_in_effect:
|
if self.restriction_in_effect:
|
||||||
self.set_number_of_books_shown(compute_count=False)
|
self.set_number_of_books_shown()
|
||||||
|
|
||||||
def mark_restriction_set(self, r):
|
def apply_search_restriction(self, r):
|
||||||
self.restriction_in_effect = False if r is None or not r else True
|
r = unicode(r)
|
||||||
|
if r is not None and r != '':
|
||||||
|
self.restriction_in_effect = True
|
||||||
|
restriction = "search:%s"%(r)
|
||||||
|
else:
|
||||||
|
self.restriction_in_effect = False
|
||||||
|
restriction = ''
|
||||||
|
self.restriction_count_of_books_in_view = \
|
||||||
|
self.library_view.model().set_search_restriction(restriction)
|
||||||
|
self.search.clear_to_help()
|
||||||
|
self.saved_search.clear_to_help()
|
||||||
|
self.tags_view.set_search_restriction(restriction)
|
||||||
|
self.set_number_of_books_shown()
|
||||||
|
|
||||||
def set_number_of_books_shown(self, compute_count):
|
def set_number_of_books_shown(self):
|
||||||
if self.current_view() == self.library_view and self.restriction_in_effect:
|
if self.current_view() == self.library_view and self.restriction_in_effect:
|
||||||
if compute_count:
|
|
||||||
self.restriction_count_of_books_in_view = self.current_view().row_count()
|
|
||||||
t = _("({0} of {1})").format(self.current_view().row_count(),
|
t = _("({0} of {1})").format(self.current_view().row_count(),
|
||||||
self.restriction_count_of_books_in_view)
|
self.restriction_count_of_books_in_view)
|
||||||
self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }')
|
self.search_count.setStyleSheet('QLabel { border-radius: 8px; background-color: yellow; }')
|
||||||
@ -873,18 +880,31 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_count.setText(t)
|
self.search_count.setText(t)
|
||||||
|
|
||||||
def search_box_cleared(self):
|
def search_box_cleared(self):
|
||||||
self.set_number_of_books_shown(compute_count=True)
|
|
||||||
self.tags_view.clear()
|
self.tags_view.clear()
|
||||||
self.saved_search.clear_to_help()
|
self.saved_search.clear_to_help()
|
||||||
|
self.set_number_of_books_shown()
|
||||||
def search_clear(self):
|
|
||||||
self.set_number_of_books_shown(compute_count=True)
|
|
||||||
self.search.clear()
|
|
||||||
|
|
||||||
def search_done(self, view, ok):
|
def search_done(self, view, ok):
|
||||||
if view is self.current_view():
|
if view is self.current_view():
|
||||||
self.search.search_done(ok)
|
self.search.search_done(ok)
|
||||||
self.set_number_of_books_shown(compute_count=False)
|
self.set_number_of_books_shown()
|
||||||
|
|
||||||
|
def saved_searches_changed(self):
|
||||||
|
p = prefs['saved_searches'].keys()
|
||||||
|
p.sort()
|
||||||
|
t = unicode(self.search_restriction.currentText())
|
||||||
|
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||||
|
self.search_restriction.addItem('')
|
||||||
|
self.tags_view.recount()
|
||||||
|
for s in p:
|
||||||
|
self.search_restriction.addItem(s)
|
||||||
|
if t:
|
||||||
|
if t in p: # redo the current restriction, if there was one
|
||||||
|
self.search_restriction.setCurrentIndex(self.search_restriction.findText(t))
|
||||||
|
# self.tags_view.set_search_restriction(t)
|
||||||
|
else:
|
||||||
|
self.search_restriction.setCurrentIndex(0)
|
||||||
|
self.apply_search_restriction('')
|
||||||
|
|
||||||
def sync_cf_to_listview(self, current, previous):
|
def sync_cf_to_listview(self, current, previous):
|
||||||
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
|
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
|
||||||
@ -2293,14 +2313,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
def library_moved(self, newloc):
|
def library_moved(self, newloc):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
db = LibraryDatabase2(newloc)
|
db = LibraryDatabase2(newloc)
|
||||||
|
self.library_path = newloc
|
||||||
self.book_on_device(None, reset=True)
|
self.book_on_device(None, reset=True)
|
||||||
db.set_book_on_device_func(self.book_on_device)
|
db.set_book_on_device_func(self.book_on_device)
|
||||||
self.library_view.set_database(db)
|
self.library_view.set_database(db)
|
||||||
|
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
||||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||||
self.status_bar.clearMessage()
|
self.status_bar.clearMessage()
|
||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
|
prefs['library_path'] = self.library_path
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
@ -2347,7 +2370,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_restriction.setEnabled(False)
|
self.search_restriction.setEnabled(False)
|
||||||
for action in list(self.delete_menu.actions())[1:]:
|
for action in list(self.delete_menu.actions())[1:]:
|
||||||
action.setEnabled(False)
|
action.setEnabled(False)
|
||||||
self.set_number_of_books_shown(compute_count=False)
|
self.set_number_of_books_shown()
|
||||||
|
|
||||||
|
|
||||||
def device_job_exception(self, job):
|
def device_job_exception(self, job):
|
||||||
|
@ -424,7 +424,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.set_bookmarks(self.iterator.bookmarks)
|
self.set_bookmarks(self.iterator.bookmarks)
|
||||||
|
|
||||||
|
|
||||||
def find(self, text, refinement, repeat=False, backwards=False):
|
def find(self, text, repeat=False, backwards=False):
|
||||||
if not text:
|
if not text:
|
||||||
self.view.search('')
|
self.view.search('')
|
||||||
return self.search.search_done(False)
|
return self.search.search_done(False)
|
||||||
|
@ -241,6 +241,24 @@ class ResultCache(SearchQueryParser):
|
|||||||
matches = set([])
|
matches = set([])
|
||||||
if len(query) < 2:
|
if len(query) < 2:
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
if location == 'date':
|
||||||
|
location = 'timestamp'
|
||||||
|
loc = self.field_metadata[location]['rec_index']
|
||||||
|
|
||||||
|
if query == 'false':
|
||||||
|
for item in self._data:
|
||||||
|
if item is None: continue
|
||||||
|
if item[loc] is None or item[loc] == UNDEFINED_DATE:
|
||||||
|
matches.add(item[0])
|
||||||
|
return matches
|
||||||
|
if query == 'true':
|
||||||
|
for item in self._data:
|
||||||
|
if item is None: continue
|
||||||
|
if item[loc] is not None and item[loc] != UNDEFINED_DATE:
|
||||||
|
matches.add(item[0])
|
||||||
|
return matches
|
||||||
|
|
||||||
relop = None
|
relop = None
|
||||||
for k in self.date_search_relops.keys():
|
for k in self.date_search_relops.keys():
|
||||||
if query.startswith(k):
|
if query.startswith(k):
|
||||||
@ -249,10 +267,6 @@ class ResultCache(SearchQueryParser):
|
|||||||
if relop is None:
|
if relop is None:
|
||||||
(p, relop) = self.date_search_relops['=']
|
(p, relop) = self.date_search_relops['=']
|
||||||
|
|
||||||
if location == 'date':
|
|
||||||
location = 'timestamp'
|
|
||||||
loc = self.field_metadata[location]['rec_index']
|
|
||||||
|
|
||||||
if query == _('today'):
|
if query == _('today'):
|
||||||
qd = now()
|
qd = now()
|
||||||
field_count = 3
|
field_count = 3
|
||||||
@ -301,7 +315,7 @@ class ResultCache(SearchQueryParser):
|
|||||||
if query == 'false':
|
if query == 'false':
|
||||||
query = '0'
|
query = '0'
|
||||||
elif query == 'true':
|
elif query == 'true':
|
||||||
query = '>0'
|
query = '!=0'
|
||||||
relop = None
|
relop = None
|
||||||
for k in self.numeric_search_relops.keys():
|
for k in self.numeric_search_relops.keys():
|
||||||
if query.startswith(k):
|
if query.startswith(k):
|
||||||
|
@ -171,6 +171,40 @@ class CustomColumns(object):
|
|||||||
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
# convenience methods for tag editing
|
||||||
|
def get_custom_items_with_ids(self, label=None, num=None):
|
||||||
|
if label is not None:
|
||||||
|
data = self.custom_column_label_map[label]
|
||||||
|
if num is not None:
|
||||||
|
data = self.custom_column_num_map[num]
|
||||||
|
table,lt = self.custom_table_names(data['num'])
|
||||||
|
if not data['normalized']:
|
||||||
|
return []
|
||||||
|
ans = self.conn.get('SELECT id, value FROM %s'%table)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def rename_custom_item(self, id, new_name, label=None, num=None):
|
||||||
|
if id:
|
||||||
|
if label is not None:
|
||||||
|
data = self.custom_column_label_map[label]
|
||||||
|
if num is not None:
|
||||||
|
data = self.custom_column_num_map[num]
|
||||||
|
table,lt = self.custom_table_names(data['num'])
|
||||||
|
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def delete_custom_item_using_id(self, id, label=None, num=None):
|
||||||
|
if id:
|
||||||
|
if label is not None:
|
||||||
|
data = self.custom_column_label_map[label]
|
||||||
|
if num is not None:
|
||||||
|
data = self.custom_column_num_map[num]
|
||||||
|
table,lt = self.custom_table_names(data['num'])
|
||||||
|
self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,))
|
||||||
|
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,))
|
||||||
|
self.conn.commit()
|
||||||
|
# end convenience methods
|
||||||
|
|
||||||
def all_custom(self, label=None, num=None):
|
def all_custom(self, label=None, num=None):
|
||||||
if label is not None:
|
if label is not None:
|
||||||
data = self.custom_column_label_map[label]
|
data = self.custom_column_label_map[label]
|
||||||
|
@ -9,12 +9,6 @@ The database used to store ebook metadata
|
|||||||
import os, sys, shutil, cStringIO, glob,functools, traceback
|
import os, sys, shutil, cStringIO, glob,functools, traceback
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import floor
|
from math import floor
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
PILImage
|
|
||||||
except ImportError:
|
|
||||||
import Image as PILImage
|
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
|
|
||||||
@ -37,7 +31,7 @@ from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
|
|||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||||
|
from calibre.utils.magick_draw import save_cover_data_to
|
||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
import calibre.utils.winshell as winshell
|
import calibre.utils.winshell as winshell
|
||||||
@ -475,11 +469,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if callable(getattr(data, 'save', None)):
|
if callable(getattr(data, 'save', None)):
|
||||||
data.save(path)
|
data.save(path)
|
||||||
else:
|
else:
|
||||||
f = data
|
if callable(getattr(data, 'read', None)):
|
||||||
if not callable(getattr(data, 'read', None)):
|
data = data.read()
|
||||||
f = cStringIO.StringIO(data)
|
save_cover_data_to(data, path)
|
||||||
im = PILImage.open(f)
|
|
||||||
im.convert('RGB').save(path, 'JPEG')
|
|
||||||
|
|
||||||
def book_on_device(self, id):
|
def book_on_device(self, id):
|
||||||
if callable(self.book_on_device_func):
|
if callable(self.book_on_device_func):
|
||||||
@ -643,11 +635,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
'''
|
'''
|
||||||
Remove orphaned entries.
|
Remove orphaned entries.
|
||||||
'''
|
'''
|
||||||
st = 'DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;'
|
def doit(ltable, table, ltable_col):
|
||||||
self.conn.execute(st%dict(ltable='authors', table='authors', ltable_col='author'))
|
st = ('DELETE FROM books_%s_link WHERE (SELECT COUNT(id) '
|
||||||
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
|
'FROM books WHERE id=book) < 1;')%ltable
|
||||||
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
|
self.conn.execute(st)
|
||||||
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
|
st = ('DELETE FROM %(table)s WHERE (SELECT COUNT(id) '
|
||||||
|
'FROM books_%(ltable)s_link WHERE '
|
||||||
|
'%(ltable_col)s=%(table)s.id) < 1;') % dict(
|
||||||
|
ltable=ltable, table=table, ltable_col=ltable_col)
|
||||||
|
self.conn.execute(st)
|
||||||
|
|
||||||
|
for ltable, table, ltable_col in [
|
||||||
|
('authors', 'authors', 'author'),
|
||||||
|
('publishers', 'publishers', 'publisher'),
|
||||||
|
('tags', 'tags', 'tag'),
|
||||||
|
('series', 'series', 'series')
|
||||||
|
]:
|
||||||
|
doit(ltable, table, ltable_col)
|
||||||
|
|
||||||
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
|
for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
|
||||||
if not tag.strip():
|
if not tag.strip():
|
||||||
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
|
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
|
||||||
@ -730,9 +735,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
||||||
icon=icon, tooltip = tooltip)
|
icon=icon, tooltip = tooltip)
|
||||||
for r in data if item_not_zero_func(r)]
|
for r in data if item_not_zero_func(r)]
|
||||||
if category == 'series':
|
if category == 'series' and not sort_on_count:
|
||||||
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name),
|
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
|
||||||
title_sort(y.name)))
|
title_sort(y.name).lower()))
|
||||||
|
|
||||||
# We delayed computing the standard formats category because it does not
|
# We delayed computing the standard formats category because it does not
|
||||||
# use a view, but is computed dynamically
|
# use a view, but is computed dynamically
|
||||||
@ -985,20 +990,92 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
# Convenience method for tags_list_editor
|
# Convenience methods for tags_list_editor
|
||||||
|
# Note: we generally do not need to refresh_ids because library_view will
|
||||||
|
# refresh everything.
|
||||||
def get_tags_with_ids(self):
|
def get_tags_with_ids(self):
|
||||||
result = self.conn.get('SELECT * FROM tags')
|
result = self.conn.get('SELECT id,name FROM tags')
|
||||||
if not result:
|
if not result:
|
||||||
return {}
|
return []
|
||||||
r = []
|
return result
|
||||||
for k,v in result:
|
|
||||||
r.append((k,v))
|
|
||||||
return r
|
|
||||||
|
|
||||||
def rename_tag(self, id, new):
|
def rename_tag(self, id, new_name):
|
||||||
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
|
if id:
|
||||||
|
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
def delete_tag_using_id(self, id):
|
||||||
|
if id:
|
||||||
|
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,))
|
||||||
|
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_series_with_ids(self):
|
||||||
|
result = self.conn.get('SELECT id,name FROM series')
|
||||||
|
if not result:
|
||||||
|
return []
|
||||||
|
return result
|
||||||
|
|
||||||
|
def rename_series(self, id, new_name):
|
||||||
|
if id:
|
||||||
|
self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def delete_series_using_id(self, id):
|
||||||
|
if id:
|
||||||
|
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
||||||
|
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
||||||
|
self.conn.execute('DELETE FROM series WHERE id=?', (id,))
|
||||||
|
self.conn.commit()
|
||||||
|
for (book_id,) in books:
|
||||||
|
self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,))
|
||||||
|
|
||||||
|
def get_publishers_with_ids(self):
|
||||||
|
result = self.conn.get('SELECT id,name FROM publishers')
|
||||||
|
if not result:
|
||||||
|
return []
|
||||||
|
return result
|
||||||
|
|
||||||
|
def rename_publisher(self, id, new_name):
|
||||||
|
if id:
|
||||||
|
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def delete_publisher_using_id(self, id):
|
||||||
|
if id:
|
||||||
|
self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,))
|
||||||
|
self.conn.execute('DELETE FROM publishers WHERE id=?', (id,))
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
# There is no editor for author, so we do not need get_authors_with_ids or
|
||||||
|
# delete_author_using_id.
|
||||||
|
def rename_author(self, id, new_name):
|
||||||
|
if id:
|
||||||
|
# Make sure that any commas in new_name are changed to '|'!
|
||||||
|
new_name = new_name.replace(',', '|')
|
||||||
|
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id))
|
||||||
|
self.conn.commit()
|
||||||
|
# now must fix up the books
|
||||||
|
books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,))
|
||||||
|
for (book_id,) in books:
|
||||||
|
# First, must refresh the cache to see the new authors
|
||||||
|
self.data.refresh_ids(self, [book_id])
|
||||||
|
# now fix the filesystem paths
|
||||||
|
self.set_path(book_id, index_is_id=True)
|
||||||
|
# Next fix the author sort. Reset it to the default
|
||||||
|
authors = self.conn.get('''
|
||||||
|
SELECT authors.name
|
||||||
|
FROM authors, books_authors_link as bl
|
||||||
|
WHERE bl.book = ? and bl.author = authors.id
|
||||||
|
''' , (book_id,))
|
||||||
|
# unpack the double-list structure
|
||||||
|
for i,aut in enumerate(authors):
|
||||||
|
authors[i] = aut[0]
|
||||||
|
ss = authors_to_sort_string(authors)
|
||||||
|
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id))
|
||||||
|
|
||||||
|
# end convenience methods
|
||||||
|
|
||||||
def get_tags(self, id):
|
def get_tags(self, id):
|
||||||
result = self.conn.get(
|
result = self.conn.get(
|
||||||
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
|
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
|
||||||
@ -1083,7 +1160,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def set_series(self, id, series, notify=True):
|
def set_series(self, id, series, notify=True):
|
||||||
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
|
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
|
||||||
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
|
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
|
||||||
@ -1603,6 +1679,7 @@ books_series_link feeds
|
|||||||
|
|
||||||
def check_integrity(self, callback):
|
def check_integrity(self, callback):
|
||||||
callback(0., _('Checking SQL integrity...'))
|
callback(0., _('Checking SQL integrity...'))
|
||||||
|
self.clean()
|
||||||
user_version = self.user_version
|
user_version = self.user_version
|
||||||
sql = '\n'.join(self.conn.dump())
|
sql = '\n'.join(self.conn.dump())
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
@ -195,11 +195,11 @@ class FieldMetadata(dict):
|
|||||||
'is_category':False}),
|
'is_category':False}),
|
||||||
('ondevice', {'table':None,
|
('ondevice', {'table':None,
|
||||||
'column':None,
|
'column':None,
|
||||||
'datatype':'bool',
|
'datatype':'text',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':None,
|
||||||
'search_terms':[],
|
'search_terms':['ondevice'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False}),
|
'is_category':False}),
|
||||||
('path', {'table':None,
|
('path', {'table':None,
|
||||||
|
@ -38,6 +38,12 @@ def server_config(defaults=None):
|
|||||||
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
|
c.add_opt('max_opds_items', ['--max-opds-items'], default=30,
|
||||||
help=_('The maximum number of matches to return per OPDS query. '
|
help=_('The maximum number of matches to return per OPDS query. '
|
||||||
'This affects Stanza, WordPlayer, etc. integration.'))
|
'This affects Stanza, WordPlayer, etc. integration.'))
|
||||||
|
c.add_opt('max_opds_ungrouped_items', ['--max-opds-ungrouped-items'],
|
||||||
|
default=100,
|
||||||
|
help=_('Group items in categories such as author/tags '
|
||||||
|
'by first letter when there are more than this number '
|
||||||
|
'of items. Default: %default. Set to a large number '
|
||||||
|
'to disable grouping.'))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -127,10 +127,7 @@ class ContentServer(object):
|
|||||||
cherrypy.log('User agent: '+ua)
|
cherrypy.log('User agent: '+ua)
|
||||||
|
|
||||||
if want_opds:
|
if want_opds:
|
||||||
return self.stanza(search=kwargs.get('search', None), sortby=kwargs.get('sortby',None), authorid=kwargs.get('authorid',None),
|
return self.opds(version=0)
|
||||||
tagid=kwargs.get('tagid',None),
|
|
||||||
seriesid=kwargs.get('seriesid',None),
|
|
||||||
offset=kwargs.get('offset', 0))
|
|
||||||
|
|
||||||
if want_mobile:
|
if want_mobile:
|
||||||
return self.mobile()
|
return self.mobile()
|
||||||
|
@ -25,7 +25,7 @@ BASE_HREFS = {
|
|||||||
1 : '/opds',
|
1 : '/opds',
|
||||||
}
|
}
|
||||||
|
|
||||||
STANZA_FORMATS = frozenset(['epub', 'pdb'])
|
STANZA_FORMATS = frozenset(['epub', 'pdb', 'pdf', 'cbr', 'cbz', 'djvu'])
|
||||||
|
|
||||||
def url_for(name, version, **kwargs):
|
def url_for(name, version, **kwargs):
|
||||||
if not name.endswith('_'):
|
if not name.endswith('_'):
|
||||||
@ -121,7 +121,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
|
|||||||
TITLE(item.text),
|
TITLE(item.text),
|
||||||
ID(id_),
|
ID(id_),
|
||||||
UPDATED(updated),
|
UPDATED(updated),
|
||||||
E.content(_('%d books')%item.count, type='text'),
|
E.content(_('%d items')%item.count, type='text'),
|
||||||
link
|
link
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -445,7 +445,7 @@ class OPDSServer(object):
|
|||||||
|
|
||||||
id_ = 'calibre-category-feed:'+which
|
id_ = 'calibre-category-feed:'+which
|
||||||
|
|
||||||
MAX_ITEMS = 50
|
MAX_ITEMS = self.opts.max_opds_ungrouped_items
|
||||||
|
|
||||||
if len(items) <= MAX_ITEMS:
|
if len(items) <= MAX_ITEMS:
|
||||||
max_items = self.opts.max_opds_items
|
max_items = self.opts.max_opds_items
|
||||||
@ -459,8 +459,6 @@ class OPDSServer(object):
|
|||||||
self.text, self.count = text, count
|
self.text, self.count = text, count
|
||||||
|
|
||||||
starts = set([x.name[0] for x in items])
|
starts = set([x.name[0] for x in items])
|
||||||
if len(starts) > MAX_ITEMS:
|
|
||||||
starts = set([x.name[:2] for x in items])
|
|
||||||
category_groups = OrderedDict()
|
category_groups = OrderedDict()
|
||||||
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
||||||
category_groups[x] = len([y for y in items if
|
category_groups[x] = len([y for y in items if
|
||||||
|
@ -8,16 +8,25 @@ Customizing |app|
|
|||||||
==================================
|
==================================
|
||||||
|
|
||||||
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
|
||||||
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn how to
|
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
|
||||||
use *plugins* to customize and control various aspects of |app|'s behavior.
|
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
|
||||||
|
use *plugins* to add funtionality to |app|.
|
||||||
Theer are different kinds of plugins, corresponding to different aspects of |app|. As more and more aspects of |app|
|
|
||||||
are modularized, new plugin types will be added.
|
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
:depth: 2
|
:depth: 2
|
||||||
:local:
|
:local:
|
||||||
|
|
||||||
|
Environment variables
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
* ``CALIBRE_CONFIG_DIRECTORY``
|
||||||
|
* ``CALIBRE_OVERRIDE_DATABASE_PATH``
|
||||||
|
* ``CALIBRE_DEVELOP_FROM``
|
||||||
|
* ``CALIBRE_OVERRIDE_LANG``
|
||||||
|
* ``SYSFS_PATH``
|
||||||
|
* ``http_proxy``
|
||||||
|
|
||||||
|
|
||||||
A Hello World plugin
|
A Hello World plugin
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -135,29 +135,18 @@ turned into a collection on the reader. Note that the PRS-500 does not support c
|
|||||||
How do I use |app| with my iPad/iPhone/iTouch?
|
How do I use |app| with my iPad/iPhone/iTouch?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can access your calibre library on a iPad/iPhone/iTouch over the air using the calibre content server.
|
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
||||||
|
|
||||||
First perform the following steps in |app|
|
First perform the following steps in |app|
|
||||||
|
|
||||||
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under Preferences->General)
|
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under Preferences->General)
|
||||||
|
* Set the output profile to iPad (this will work for iPhone/iPods as well), under Preferences->Conversion->Page Setup
|
||||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||||
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||||
|
|
||||||
For an iPad:
|
Install the free Stanza reader app on your iPad/iPhone/iTouch using iTunes.
|
||||||
|
|
||||||
Install the ReadMe app on your iPad using iTunes. Open the Readme builtin browser and browse to::
|
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
||||||
|
|
||||||
http://192.168.1.2:8080/
|
|
||||||
|
|
||||||
Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you have changed the port the |app| content server is running on, you will have to change ``8080`` as well to the new port. The local IP address is the IP address you computer is assigned on your home network. A quick Google search will tell you how to find out your local IP address.
|
|
||||||
|
|
||||||
The books in your |app| library will be presented as a list, 25 entries at a time. Click the right arrow to go to the next 25. You can also type in the search box to find specific books. Just click on the EPUB link of the book you want and it will be downloaded into your ReadMe library.
|
|
||||||
|
|
||||||
For an iPhone/iTouch:
|
|
||||||
|
|
||||||
Install the free Stanza reader app on your iPhone/iTouch using iTunes.
|
|
||||||
|
|
||||||
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
|
||||||
|
|
||||||
http://192.168.1.2:8080/
|
http://192.168.1.2:8080/
|
||||||
|
|
||||||
@ -165,7 +154,12 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
|
|||||||
|
|
||||||
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
||||||
|
|
||||||
Note that neither the Stanza, nor the ReadMe apps are in anyway associated with |app|.
|
Alternative for the iPad
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
As of |app| version 0.7.0, you can plugin your iPad into the computer using its charging cable, and |app| will detect it and show you a list of books on the iPad. You can then use the Send to device button to send books directly to iBooks on the iPad.
|
||||||
|
|
||||||
|
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
|
||||||
|
|
||||||
How do I use |app| with my Android phone?
|
How do I use |app| with my Android phone?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -241,9 +241,9 @@ Now, you can access your saved search in the Tag Browser under "Searches". A sin
|
|||||||
|
|
||||||
.. _configuration:
|
.. _configuration:
|
||||||
|
|
||||||
Configuration
|
Preferences
|
||||||
---------------
|
---------------
|
||||||
The configuration dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|.
|
The Preferences dialog allows you to change the way various aspects of |app| work. To access it, click the |cbi|.
|
||||||
|
|
||||||
.. |cbi| image:: images/configuration.png
|
.. |cbi| image:: images/configuration.png
|
||||||
|
|
||||||
@ -251,7 +251,7 @@ The configuration dialog allows you to set some global defaults used by all of |
|
|||||||
|
|
||||||
Guessing metadata from file names
|
Guessing metadata from file names
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
In the :guilabel:`Advanced` section of the configuration dialog, you can specify a regularexpression that |app| will use to try and guess metadata from the names of ebook files
|
In the :guilabel:`Add/Save` section of the configuration dialog, you can specify a regular expression that |app| will use to try and guess metadata from the names of ebook files
|
||||||
that you add to the library. The default regular expression is::
|
that you add to the library. The default regular expression is::
|
||||||
|
|
||||||
title - author
|
title - author
|
||||||
@ -265,18 +265,13 @@ will be interpreted to have the title: Foundation and Earth and author: Isaac As
|
|||||||
.. tip::
|
.. tip::
|
||||||
If the filename does not contain the hyphen, the regular expression will fail.
|
If the filename does not contain the hyphen, the regular expression will fail.
|
||||||
|
|
||||||
.. tip::
|
|
||||||
If you want to only use metadata guessed from filenames and not metadata read from the file itself, you can tell |app| to do this, via the configuration dialog, accessed by the button to the right
|
|
||||||
of the search box.
|
|
||||||
|
|
||||||
.. _book_details:
|
.. _book_details:
|
||||||
|
|
||||||
Book Details
|
Book Details
|
||||||
-------------
|
-------------
|
||||||
.. image:: images/book_details.png
|
.. image:: images/book_details.png
|
||||||
|
|
||||||
The Book Details display shows you extra information and the cover for the currently selected book. THe comments section is truncated if the comments are too long. To see the full comments as well as
|
The Book Details display shows you extra information and the cover for the currently selected book.
|
||||||
a larger image of the cover, click anywhere in the Book Details area.
|
|
||||||
|
|
||||||
.. _jobs:
|
.. _jobs:
|
||||||
|
|
||||||
|
@ -111,6 +111,8 @@ Pre/post processing of downloaded HTML
|
|||||||
|
|
||||||
.. automember:: BasicNewsRecipe.remove_javascript
|
.. automember:: BasicNewsRecipe.remove_javascript
|
||||||
|
|
||||||
|
.. automethod:: BasicNewsRecipe.prepreprocess_html
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.preprocess_html
|
.. automethod:: BasicNewsRecipe.preprocess_html
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.postprocess_html
|
.. automethod:: BasicNewsRecipe.postprocess_html
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -161,6 +161,19 @@ def create_text_arc(text, font_size, font=None, bgcolor='white'):
|
|||||||
p.MagickTrimImage(canvas, 0)
|
p.MagickTrimImage(canvas, 0)
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0,
|
||||||
|
border_color='white'):
|
||||||
|
with p.ImageMagick():
|
||||||
|
img = load_image(path_to_image)
|
||||||
|
lwidth = p.MagickGetImageWidth(img)
|
||||||
|
lheight = p.MagickGetImageHeight(img)
|
||||||
|
canvas = create_canvas(lwidth+left+right, lheight+top+bottom,
|
||||||
|
border_color)
|
||||||
|
compose_image(canvas, img, left, top)
|
||||||
|
p.DestroyMagickWand(img)
|
||||||
|
with open(path_to_image, 'wb') as f:
|
||||||
|
p.MagickWriteImage(canvas, f)
|
||||||
|
p.DestroyMagickWand(canvas)
|
||||||
|
|
||||||
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||||
bgcolor='white', output_format='png'):
|
bgcolor='white', output_format='png'):
|
||||||
@ -199,6 +212,23 @@ def create_cover_page(top_lines, logo_path, width=590, height=750,
|
|||||||
p.DestroyMagickWand(canvas)
|
p.DestroyMagickWand(canvas)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def save_cover_data_to(data, path, bgcolor='white'):
|
||||||
|
'''
|
||||||
|
Saves image in data to path, in the format specified by the path
|
||||||
|
extension. Composes the image onto a blank cancas so as to
|
||||||
|
properly convert transparent images.
|
||||||
|
'''
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
with p.ImageMagick():
|
||||||
|
img = load_image(path)
|
||||||
|
canvas = create_canvas(p.MagickGetImageWidth(img),
|
||||||
|
p.MagickGetImageHeight(img), bgcolor)
|
||||||
|
compose_image(canvas, img, 0, 0)
|
||||||
|
p.MagickWriteImage(canvas, path)
|
||||||
|
p.DestroyMagickWand(img)
|
||||||
|
p.DestroyMagickWand(canvas)
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
import subprocess
|
import subprocess
|
||||||
with TemporaryFile('.png') as f:
|
with TemporaryFile('.png') as f:
|
||||||
|
@ -52,6 +52,12 @@ class SavedSearchQueries(object):
|
|||||||
self.queries.pop(self.force_unicode(name), False)
|
self.queries.pop(self.force_unicode(name), False)
|
||||||
prefs[self.opt_name] = self.queries
|
prefs[self.opt_name] = self.queries
|
||||||
|
|
||||||
|
def rename(self, old_name, new_name):
|
||||||
|
self.queries[self.force_unicode(new_name)] = \
|
||||||
|
self.queries.get(self.force_unicode(old_name), None)
|
||||||
|
self.queries.pop(self.force_unicode(old_name), False)
|
||||||
|
prefs[self.opt_name] = self.queries
|
||||||
|
|
||||||
def names(self):
|
def names(self):
|
||||||
return sorted(self.queries.keys(),
|
return sorted(self.queries.keys(),
|
||||||
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||||
|
@ -11,7 +11,7 @@ from lxml import html
|
|||||||
|
|
||||||
from calibre.web.feeds.feedparser import parse
|
from calibre.web.feeds.feedparser import parse
|
||||||
from calibre.utils.logging import default_log
|
from calibre.utils.logging import default_log
|
||||||
from calibre import entity_to_unicode
|
from calibre import entity_to_unicode, strftime
|
||||||
from calibre.utils.date import dt_factory, utcnow, local_tz
|
from calibre.utils.date import dt_factory, utcnow, local_tz
|
||||||
|
|
||||||
class Article(object):
|
class Article(object):
|
||||||
@ -53,12 +53,17 @@ class Article(object):
|
|||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def formatted_date(self):
|
def formatted_date(self):
|
||||||
|
|
||||||
def fget(self):
|
def fget(self):
|
||||||
if self._formatted_date is None:
|
if self._formatted_date is None:
|
||||||
self._formatted_date = self.localtime.strftime(" [%a, %d %b %H:%M]")
|
self._formatted_date = strftime(" [%a, %d %b %H:%M]",
|
||||||
|
t=self.localtime.timetuple())
|
||||||
return self._formatted_date
|
return self._formatted_date
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
|
if isinstance(val, unicode):
|
||||||
self._formatted_date = val
|
self._formatted_date = val
|
||||||
|
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
|
@ -267,7 +267,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.article {
|
a.article {
|
||||||
font-weight: bold;
|
font-weight: bold; text-align:left;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.feed {
|
a.feed {
|
||||||
@ -403,10 +403,25 @@ class BasicNewsRecipe(Recipe):
|
|||||||
return url
|
return url
|
||||||
return article.get('link', None)
|
return article.get('link', None)
|
||||||
|
|
||||||
|
def prepreprocess_html(self, soup):
|
||||||
|
'''
|
||||||
|
This method is called with the source of each downloaded :term:`HTML` file, before
|
||||||
|
any of the cleanup attributes like remove_tags, keep_only_tags are
|
||||||
|
applied. Note that preprocess_regexps will have already been applied.
|
||||||
|
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
|
||||||
|
It should return `soup` after processing it.
|
||||||
|
|
||||||
|
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
|
||||||
|
instance containing the downloaded :term:`HTML`.
|
||||||
|
'''
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
'''
|
'''
|
||||||
This method is called with the source of each downloaded :term:`HTML` file, before
|
This method is called with the source of each downloaded :term:`HTML` file, before
|
||||||
it is parsed for links and images.
|
it is parsed for links and images. It is called after the cleanup as
|
||||||
|
specified by remove_tags etc.
|
||||||
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
|
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
|
||||||
It should return `soup` after processing it.
|
It should return `soup` after processing it.
|
||||||
|
|
||||||
@ -523,7 +538,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
Intended to be used to get article metadata like author/summary/etc.
|
Intended to be used to get article metadata like author/summary/etc.
|
||||||
from the parsed HTML (soup).
|
from the parsed HTML (soup).
|
||||||
:param article: A object of class :class:`calibre.web.feeds.Article`.
|
:param article: A object of class :class:`calibre.web.feeds.Article`.
|
||||||
If you chane the sumamry, remeber to also change the
|
If you change the summary, remember to also change the
|
||||||
text_summary
|
text_summary
|
||||||
:param soup: Parsed HTML belonging to this article
|
:param soup: Parsed HTML belonging to this article
|
||||||
:param first: True iff the parsed HTML is the first page of the article.
|
:param first: True iff the parsed HTML is the first page of the article.
|
||||||
@ -603,7 +618,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
|
|
||||||
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
|
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
|
||||||
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
|
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
|
||||||
'preprocess_html', 'remove_tags_after',
|
'prepreprocess_html', 'preprocess_html', 'remove_tags_after',
|
||||||
'remove_tags_before', 'is_link_wanted'):
|
'remove_tags_before', 'is_link_wanted'):
|
||||||
setattr(self.web2disk_options, extra, getattr(self, extra))
|
setattr(self.web2disk_options, extra, getattr(self, extra))
|
||||||
self.web2disk_options.postprocess_html = self._postprocess_html
|
self.web2disk_options.postprocess_html = self._postprocess_html
|
||||||
@ -758,15 +773,15 @@ class BasicNewsRecipe(Recipe):
|
|||||||
if self.touchscreen:
|
if self.touchscreen:
|
||||||
touchscreen_css = u'''
|
touchscreen_css = u'''
|
||||||
.summary_headline {
|
.summary_headline {
|
||||||
font-size:large; font-weight:bold; margin-top:0px; margin-bottom:0px;
|
font-weight:bold; text-align:left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary_byline {
|
.summary_byline {
|
||||||
font-size:small; margin-top:0px; margin-bottom:0px;
|
font-family:monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary_text {
|
.summary_text {
|
||||||
margin-top:0px; margin-bottom:0px;
|
text-align:left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed {
|
.feed {
|
||||||
@ -782,12 +797,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
border-width:thin;
|
border-width:thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.toc {
|
|
||||||
font-size:large;
|
|
||||||
}
|
|
||||||
td.article_count {
|
|
||||||
text-align:right;
|
|
||||||
}
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
templ = templates.TouchscreenFeedTemplate()
|
templ = templates.TouchscreenFeedTemplate()
|
||||||
@ -1120,8 +1129,11 @@ class BasicNewsRecipe(Recipe):
|
|||||||
mi.publisher = __appname__
|
mi.publisher = __appname__
|
||||||
mi.author_sort = __appname__
|
mi.author_sort = __appname__
|
||||||
if self.output_profile.name == 'iPad':
|
if self.output_profile.name == 'iPad':
|
||||||
mi.authors = [strftime('%A, %d %B %Y')]
|
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
mi.author_sort = strftime('%Y-%m-%d')
|
mi = MetaInformation(self.short_title(), [date_as_author])
|
||||||
|
mi.publisher = __appname__
|
||||||
|
sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
||||||
|
mi.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
|
||||||
mi.publication_type = 'periodical:'+self.publication_type
|
mi.publication_type = 'periodical:'+self.publication_type
|
||||||
mi.timestamp = nowf()
|
mi.timestamp = nowf()
|
||||||
mi.comments = self.description
|
mi.comments = self.description
|
||||||
@ -1245,7 +1257,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
with nested(open(opf_path, 'wb'), open(ncx_path, 'wb')) as (opf_file, ncx_file):
|
with nested(open(opf_path, 'wb'), open(ncx_path, 'wb')) as (opf_file, ncx_file):
|
||||||
opf.render(opf_file, ncx_file)
|
opf.render(opf_file, ncx_file)
|
||||||
|
|
||||||
|
|
||||||
def article_downloaded(self, request, result):
|
def article_downloaded(self, request, result):
|
||||||
index = os.path.join(os.path.dirname(result[0]), 'index.html')
|
index = os.path.join(os.path.dirname(result[0]), 'index.html')
|
||||||
if index != result[0]:
|
if index != result[0]:
|
||||||
|
@ -241,7 +241,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
|||||||
results.add(urn)
|
results.add(urn)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def search(self, query, refinement):
|
def search(self, query):
|
||||||
try:
|
try:
|
||||||
results = self.parse(unicode(query))
|
results = self.parse(unicode(query))
|
||||||
if not results:
|
if not results:
|
||||||
|
@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
from lxml import html, etree
|
from lxml import html, etree
|
||||||
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
|
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
|
||||||
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
|
STRONG, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
|
||||||
TABLE, TD, TR
|
TABLE, TD, TR
|
||||||
|
|
||||||
from calibre import preferred_encoding, strftime, isbytestring
|
from calibre import preferred_encoding, strftime, isbytestring
|
||||||
@ -120,6 +120,7 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
href = '%s%s/%s/index.html'%(prefix, up, next)
|
href = '%s%s/%s/index.html'%(prefix, up, next)
|
||||||
navbar.text = '| '
|
navbar.text = '| '
|
||||||
navbar.append(A('Next', href=href))
|
navbar.append(A('Next', href=href))
|
||||||
|
|
||||||
href = '%s../index.html#article_%d'%(prefix, art)
|
href = '%s../index.html#article_%d'%(prefix, art)
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
navbar.append(A('Section Menu', href=href))
|
navbar.append(A('Section Menu', href=href))
|
||||||
@ -130,6 +131,7 @@ class TouchscreenNavBarTemplate(Template):
|
|||||||
href = '%s../article_%d/index.html'%(prefix, art-1)
|
href = '%s../article_%d/index.html'%(prefix, art-1)
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
navbar.append(A('Previous', href=href))
|
navbar.append(A('Previous', href=href))
|
||||||
|
|
||||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||||
if not bottom:
|
if not bottom:
|
||||||
navbar.append(HR())
|
navbar.append(HR())
|
||||||
@ -165,8 +167,14 @@ class TouchscreenIndexTemplate(Template):
|
|||||||
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
|
def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None):
|
||||||
if isinstance(datefmt, unicode):
|
if isinstance(datefmt, unicode):
|
||||||
datefmt = datefmt.encode(preferred_encoding)
|
datefmt = datefmt.encode(preferred_encoding)
|
||||||
date = strftime(datefmt)
|
date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
||||||
masthead_img = IMG(src=masthead,alt="masthead")
|
masthead_p = etree.Element("p")
|
||||||
|
masthead_p.set("style","text-align:center")
|
||||||
|
masthead_img = etree.Element("img")
|
||||||
|
masthead_img.set("src",masthead)
|
||||||
|
masthead_img.set("alt","masthead")
|
||||||
|
masthead_p.append(masthead_img)
|
||||||
|
|
||||||
head = HEAD(TITLE(title))
|
head = HEAD(TITLE(title))
|
||||||
if style:
|
if style:
|
||||||
head.append(STYLE(style, type='text/css'))
|
head.append(STYLE(style, type='text/css'))
|
||||||
@ -177,15 +185,13 @@ class TouchscreenIndexTemplate(Template):
|
|||||||
for i, feed in enumerate(feeds):
|
for i, feed in enumerate(feeds):
|
||||||
if feed:
|
if feed:
|
||||||
tr = TR()
|
tr = TR()
|
||||||
tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i)))
|
tr.append(TD( CLASS('calibre_rescale_120'), A(feed.title, href='feed_%d/index.html'%i)))
|
||||||
tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles)))
|
tr.append(TD( '%s' % len(feed.articles), style="text-align:right"))
|
||||||
toc.append(tr)
|
toc.append(tr)
|
||||||
|
|
||||||
div = DIV(
|
div = DIV(
|
||||||
PT(masthead_img,style='text-align:center'),
|
masthead_p,
|
||||||
PT(date, style='text-align:center'),
|
PT(date, style='text-align:center'),
|
||||||
toc,
|
toc)
|
||||||
CLASS('calibre_rescale_100'))
|
|
||||||
self.root = HTML(head, BODY(div))
|
self.root = HTML(head, BODY(div))
|
||||||
|
|
||||||
class FeedTemplate(Template):
|
class FeedTemplate(Template):
|
||||||
@ -271,12 +277,15 @@ class TouchscreenFeedTemplate(Template):
|
|||||||
continue
|
continue
|
||||||
tr = TR()
|
tr = TR()
|
||||||
td = TD(
|
td = TD(
|
||||||
A(article.title, CLASS('article calibre_rescale_100',
|
A(article.title, CLASS('summary_headline','calibre_rescale_120',
|
||||||
href=article.url))
|
href=article.url))
|
||||||
)
|
)
|
||||||
|
if article.author:
|
||||||
|
td.append(DIV(article.author,
|
||||||
|
CLASS('summary_byline', 'calibre_rescale_100')))
|
||||||
if article.summary:
|
if article.summary:
|
||||||
td.append(DIV(cutoff(article.text_summary),
|
td.append(DIV(cutoff(article.text_summary),
|
||||||
CLASS('article_description', 'calibre_rescale_80')))
|
CLASS('summary_text', 'calibre_rescale_100')))
|
||||||
tr.append(td)
|
tr.append(td)
|
||||||
toc.append(tr)
|
toc.append(tr)
|
||||||
div.append(toc)
|
div.append(toc)
|
||||||
|
@ -136,6 +136,7 @@ class RecursiveFetcher(object):
|
|||||||
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
|
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
|
||||||
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
|
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
|
||||||
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
|
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
|
||||||
|
self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup)
|
||||||
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
|
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
|
||||||
self._is_link_wanted = getattr(options, 'is_link_wanted',
|
self._is_link_wanted = getattr(options, 'is_link_wanted',
|
||||||
default_is_link_wanted)
|
default_is_link_wanted)
|
||||||
@ -153,6 +154,8 @@ class RecursiveFetcher(object):
|
|||||||
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
|
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
|
||||||
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
|
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
|
||||||
|
|
||||||
|
soup = self.prepreprocess_html_ext(soup)
|
||||||
|
|
||||||
if self.keep_only_tags:
|
if self.keep_only_tags:
|
||||||
body = Tag(soup, 'body')
|
body = Tag(soup, 'body')
|
||||||
try:
|
try:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user