Pull from trunk
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 396 KiB |
BIN
resources/images/news/akter.png
Normal file
After Width: | Height: | Size: 429 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 140 KiB |
78
resources/recipes/akter.recipe
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
akter.co.rs
|
||||||
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Akter(BasicNewsRecipe):
|
||||||
|
title = 'AKTER'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'AKTER - nedeljni politicki magazin savremene Srbije'
|
||||||
|
publisher = 'Akter Media Group d.o.o.'
|
||||||
|
category = 'vesti, online vesti, najnovije vesti, politika, sport, ekonomija, biznis, finansije, berza, kultura, zivot, putovanja, auto, automobili, tehnologija, politicki magazin, dogadjaji, desavanja, lifestyle, zdravlje, zdravstvo, vest, novine, nedeljnik, srbija, novi sad, vojvodina, svet, drustvo, zabava, republika srpska, beograd, intervju, komentar, reportaza, arhiva vesti, news, serbia, politics'
|
||||||
|
oldest_article = 8
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = False
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'utf-8'
|
||||||
|
masthead_url = 'http://www.akter.co.rs/templates/gk_thenews2/images/style2/logo.png'
|
||||||
|
language = 'sr'
|
||||||
|
publication_type = 'magazine'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
PREFIX = 'http://www.akter.co.rs'
|
||||||
|
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
||||||
|
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||||
|
.article_description,body,.lokacija{font-family: Arial,Helvetica,sans1,sans-serif}
|
||||||
|
.color-2{display:block; margin-bottom: 10px; padding: 5px, 10px;
|
||||||
|
border-left: 1px solid #D00000; color: #D00000}
|
||||||
|
img{margin-bottom: 0.8em} """
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
, 'linearize_tables' : True
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Politika' , u'http://www.akter.co.rs/index.php/politikaprint.html' )
|
||||||
|
,(u'Ekonomija' , u'http://www.akter.co.rs/index.php/ekonomijaprint.html')
|
||||||
|
,(u'Life&Style' , u'http://www.akter.co.rs/index.php/lsprint.html' )
|
||||||
|
,(u'Sport' , u'http://www.akter.co.rs/index.php/sportprint.html' )
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + '?tmpl=component&print=1&page='
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
totalfeeds = []
|
||||||
|
lfeeds = self.get_feeds()
|
||||||
|
for feedobj in lfeeds:
|
||||||
|
feedtitle, feedurl = feedobj
|
||||||
|
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||||
|
articles = []
|
||||||
|
soup = self.index_to_soup(feedurl)
|
||||||
|
for item in soup.findAll(attrs={'class':['sectiontableentry1','sectiontableentry2']}):
|
||||||
|
link = item.find('a')
|
||||||
|
url = self.PREFIX + link['href']
|
||||||
|
title = self.tag_to_string(link)
|
||||||
|
articles.append({
|
||||||
|
'title' :title
|
||||||
|
,'date' :''
|
||||||
|
,'url' :url
|
||||||
|
,'description':''
|
||||||
|
})
|
||||||
|
totalfeeds.append((feedtitle, articles))
|
||||||
|
return totalfeeds
|
||||||
|
|
@ -1,41 +1,43 @@
|
|||||||
"""
|
#!/usr/bin/env python
|
||||||
publico.py - v1.0
|
__author__ = u'Jordi Balcells'
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
description = u'Jornal portugu\xeas - v1.03 (16 June 2010)'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
Copyright (c) 2009, David Rodrigues - http://sixhat.net
|
'''
|
||||||
All rights reserved.
|
publico.pt
|
||||||
"""
|
'''
|
||||||
|
|
||||||
__license__ = 'GPL 3'
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
import re
|
|
||||||
|
|
||||||
class Publico(BasicNewsRecipe):
|
class PublicoPT(BasicNewsRecipe):
|
||||||
title = u'P\xfablico'
|
description = u'Jornal portugu\xeas'
|
||||||
__author__ = 'David Rodrigues'
|
cover_url = 'http://static.publico.pt/files/header/img/publico.gif'
|
||||||
oldest_article = 1
|
title = u'Publico.PT'
|
||||||
max_articles_per_feed = 30
|
category = 'News, politics, culture, economy, general interest'
|
||||||
encoding='utf-8'
|
oldest_article = 2
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
language = 'pt'
|
encoding = 'utf8'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'pt'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} '
|
||||||
|
|
||||||
preprocess_regexps = [(re.compile(u"\uFFFD", re.DOTALL|re.IGNORECASE), lambda match: ''),]
|
keep_only_tags = [dict(attrs={'class':['content-noticia-title','artigoHeader','ECOSFERA_MANCHETE','noticia','textoPrincipal','ECOSFERA_texto_01']})]
|
||||||
|
remove_tags = [dict(attrs={'class':['options','subcoluna']})]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Geral', u'http://feeds.feedburner.com/PublicoUltimaHora'),
|
(u'Geral', u'http://feeds.feedburner.com/publicoRSS'),
|
||||||
(u'Internacional', u'http://www.publico.clix.pt/rss.ashx?idCanal=11'),
|
(u'Mundo', u'http://feeds.feedburner.com/PublicoMundo'),
|
||||||
(u'Pol\xedtica', u'http://www.publico.clix.pt/rss.ashx?idCanal=12'),
|
(u'Pol\xedtica', u'http://feeds.feedburner.com/PublicoPolitica'),
|
||||||
(u'Ci\xcencias', u'http://www.publico.clix.pt/rss.ashx?idCanal=13'),
|
(u'Economia', u'http://feeds.feedburner.com/PublicoEconomia'),
|
||||||
(u'Desporto', u'http://desporto.publico.pt/rss.ashx'),
|
(u'Desporto', u'http://feeds.feedburner.com/PublicoDesporto'),
|
||||||
(u'Economia', u'http://www.publico.clix.pt/rss.ashx?idCanal=57'),
|
(u'Sociedade', u'http://feeds.feedburner.com/PublicoSociedade'),
|
||||||
(u'Educa\xe7\xe3o', u'http://www.publico.clix.pt/rss.ashx?idCanal=58'),
|
(u'Educa\xe7\xe3o', u'http://feeds.feedburner.com/PublicoEducacao'),
|
||||||
(u'Local', u'http://www.publico.clix.pt/rss.ashx?idCanal=59'),
|
(u'Ci\xeancias', u'http://feeds.feedburner.com/PublicoCiencias'),
|
||||||
(u'Media e Tecnologia', u'http://www.publico.clix.pt/rss.ashx?idCanal=61'),
|
(u'Ecosfera', u'http://feeds.feedburner.com/PublicoEcosfera'),
|
||||||
(u'Sociedade', u'http://www.publico.clix.pt/rss.ashx?idCanal=62')
|
(u'Cultura', u'http://feeds.feedburner.com/PublicoCultura'),
|
||||||
]
|
(u'Local', u'http://feeds.feedburner.com/PublicoLocal'),
|
||||||
remove_tags = [dict(name='script'), dict(id='linhaTitulosHeader')]
|
(u'Tecnologia', u'http://feeds.feedburner.com/PublicoTecnologia')
|
||||||
keep_only_tags = [dict(name='div')]
|
]
|
||||||
|
|
||||||
def print_version(self,url):
|
|
||||||
s=re.findall("id=[0-9]+",url);
|
|
||||||
return "http://ww2.publico.clix.pt/print.aspx?"+s[0]
|
|
||||||
|
@ -10,8 +10,10 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class Slashdot(BasicNewsRecipe):
|
class Slashdot(BasicNewsRecipe):
|
||||||
title = u'Slashdot.org'
|
title = u'Slashdot.org'
|
||||||
description = '''Tech news. WARNING: This recipe downloads a lot
|
description = '''Tech news. WARNING: This recipe downloads a lot
|
||||||
of content and can result in your IP being banned from slashdot.org'''
|
of content and may result in your IP being banned from slashdot.org'''
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
|
simultaneous_downloads = 1
|
||||||
|
delay = 3
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
|
@ -3,126 +3,141 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre import strftime
|
|
||||||
|
|
||||||
# http://online.wsj.com/page/us_in_todays_paper.html
|
# http://online.wsj.com/page/us_in_todays_paper.html
|
||||||
|
|
||||||
class WallStreetJournal(BasicNewsRecipe):
|
class WallStreetJournal(BasicNewsRecipe):
|
||||||
|
|
||||||
title = 'The Wall Street Journal (US)'
|
title = 'The Wall Street Journal (US)'
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
__author__ = 'Kovid Goyal and Sujata Raman'
|
||||||
description = 'News and current affairs'
|
description = 'News and current affairs'
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
max_articles_per_feed = 1000
|
max_articles_per_feed = 1000
|
||||||
timefmt = ' [%a, %b %d, %Y]'
|
timefmt = ' [%a, %b %d, %Y]'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; }
|
extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; }
|
||||||
h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
||||||
.subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
.subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
|
||||||
.insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small }
|
.insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small }
|
||||||
.targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif}
|
.targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif}
|
||||||
.article{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
|
.article{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
|
||||||
.tagline {color:#333333; font-size:xx-small}
|
.tagline {color:#333333; font-size:xx-small}
|
||||||
.dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif}
|
.dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif}
|
||||||
h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
||||||
.byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
.byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
|
||||||
h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; }
|
h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; }
|
||||||
.paperLocation{color:#666666; font-size:xx-small}'''
|
.paperLocation{color:#666666; font-size:xx-small}'''
|
||||||
|
|
||||||
remove_tags_before = dict(name='h1')
|
remove_tags_before = dict(name='h1')
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
|
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
|
||||||
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
||||||
dict(rel='shortcut icon'),
|
dict(rel='shortcut icon'),
|
||||||
]
|
]
|
||||||
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
||||||
|
|
||||||
|
|
||||||
def get_browser(self):
|
def get_browser(self):
|
||||||
br = BasicNewsRecipe.get_browser()
|
br = BasicNewsRecipe.get_browser()
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
br.open('http://commerce.wsj.com/auth/login')
|
br.open('http://commerce.wsj.com/auth/login')
|
||||||
br.select_form(nr=0)
|
br.select_form(nr=0)
|
||||||
br['user'] = self.username
|
br['user'] = self.username
|
||||||
br['password'] = self.password
|
br['password'] = self.password
|
||||||
res = br.submit()
|
res = br.submit()
|
||||||
raw = res.read()
|
raw = res.read()
|
||||||
if 'Welcome,' not in raw:
|
if 'Welcome,' not in raw:
|
||||||
raise ValueError('Failed to log in to wsj.com, check your '
|
raise ValueError('Failed to log in to wsj.com, check your '
|
||||||
'username and password')
|
'username and password')
|
||||||
return br
|
return br
|
||||||
|
|
||||||
def postprocess_html(self, soup, first):
|
def postprocess_html(self, soup, first):
|
||||||
for tag in soup.findAll(name=['table', 'tr', 'td']):
|
for tag in soup.findAll(name=['table', 'tr', 'td']):
|
||||||
tag.name = 'div'
|
tag.name = 'div'
|
||||||
|
|
||||||
for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])):
|
for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])):
|
||||||
tag.extract()
|
tag.extract()
|
||||||
|
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
def wsj_get_index(self):
|
def wsj_get_index(self):
|
||||||
return self.index_to_soup('http://online.wsj.com/page/us_in_todays_paper.html')
|
return self.index_to_soup('http://online.wsj.com/itp')
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
soup = self.wsj_get_index()
|
soup = self.wsj_get_index()
|
||||||
|
|
||||||
year = strftime('%Y')
|
date = soup.find('span', attrs={'class':'date-date'})
|
||||||
for x in soup.findAll('td', height='25', attrs={'class':'b14'}):
|
if date is not None:
|
||||||
txt = self.tag_to_string(x).strip()
|
self.timefmt = ' [%s]'%self.tag_to_string(date)
|
||||||
txt = txt.replace(u'\xa0', ' ')
|
|
||||||
txt = txt.encode('ascii', 'ignore')
|
|
||||||
if year in txt:
|
|
||||||
self.timefmt = ' [%s]'%txt
|
|
||||||
break
|
|
||||||
|
|
||||||
left_column = soup.find(
|
cov = soup.find('a', attrs={'class':'icon pdf'}, href=True)
|
||||||
text=lambda t: 'begin ITP Left Column' in str(t))
|
if cov is not None:
|
||||||
|
self.cover_url = cov['href']
|
||||||
|
|
||||||
table = left_column.findNext('table')
|
feeds = []
|
||||||
|
div = soup.find('div', attrs={'class':'itpHeader'})
|
||||||
|
div = div.find('ul', attrs={'class':'tab'})
|
||||||
|
for a in div.findAll('a', href=lambda x: x and '/itp/' in x):
|
||||||
|
title = self.tag_to_string(a)
|
||||||
|
url = 'http://online.wsj.com' + a['href']
|
||||||
|
self.log('Found section:', title)
|
||||||
|
articles = self.wsj_find_articles(url)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
|
||||||
current_section = None
|
return feeds
|
||||||
current_articles = []
|
|
||||||
feeds = []
|
|
||||||
for x in table.findAllNext(True):
|
|
||||||
if x.name == 'td' and x.get('class', None) == 'b13':
|
|
||||||
if current_articles and current_section:
|
|
||||||
feeds.append((current_section, current_articles))
|
|
||||||
current_section = self.tag_to_string(x.a).strip()
|
|
||||||
current_articles = []
|
|
||||||
self.log('\tProcessing section:', current_section)
|
|
||||||
if current_section is not None and x.name == 'a' and \
|
|
||||||
x.get('class', None) == 'bold80':
|
|
||||||
title = self.tag_to_string(x)
|
|
||||||
url = x.get('href', False)
|
|
||||||
if not url or not title:
|
|
||||||
continue
|
|
||||||
url = url.partition('#')[0]
|
|
||||||
desc = ''
|
|
||||||
d = x.findNextSibling(True)
|
|
||||||
if d is not None and d.get('class', None) == 'arialResize':
|
|
||||||
desc = self.tag_to_string(d)
|
|
||||||
desc = desc.partition(u'\u2022')[0]
|
|
||||||
self.log('\t\tFound article:', title)
|
|
||||||
self.log('\t\t\t', url)
|
|
||||||
if url.startswith('/'):
|
|
||||||
url = 'http://online.wsj.com'+url
|
|
||||||
if desc:
|
|
||||||
self.log('\t\t\t', desc)
|
|
||||||
current_articles.append({'title': title, 'url':url,
|
|
||||||
'description':desc, 'date':''})
|
|
||||||
|
|
||||||
if current_articles and current_section:
|
def wsj_find_articles(self, url):
|
||||||
feeds.append((current_section, current_articles))
|
soup = self.index_to_soup(url)
|
||||||
|
|
||||||
return feeds
|
whats_news = soup.find('div', attrs={'class':lambda x: x and
|
||||||
|
'whatsNews-simple' in x})
|
||||||
|
if whats_news is not None:
|
||||||
|
whats_news.extract()
|
||||||
|
|
||||||
def cleanup(self):
|
articles = []
|
||||||
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
|
||||||
|
for a in soup.findAll('a', attrs={'class':'mjLinkItem'}, href=True):
|
||||||
|
container = a.findParent(['li', 'div'])
|
||||||
|
meta = a.find(attrs={'class':'meta_sectionName'})
|
||||||
|
if meta is not None:
|
||||||
|
meta.extract()
|
||||||
|
title = self.tag_to_string(a).strip() + ' [%s]'%self.tag_to_string(meta)
|
||||||
|
url = 'http://online.wsj.com'+a['href']
|
||||||
|
desc = ''
|
||||||
|
p = container.find('p')
|
||||||
|
if p is not None:
|
||||||
|
desc = self.tag_to_string(p)
|
||||||
|
|
||||||
|
articles.append({'title':title, 'url':url,
|
||||||
|
'description':desc, 'date':''})
|
||||||
|
|
||||||
|
self.log('\tFound article:', title)
|
||||||
|
|
||||||
|
'''
|
||||||
|
# Find related articles
|
||||||
|
a.extract()
|
||||||
|
for a in container.findAll('a', href=lambda x: x and '/article/'
|
||||||
|
in x and 'articleTabs' not in x):
|
||||||
|
url = a['href']
|
||||||
|
if not url.startswith('http:'):
|
||||||
|
url = 'http://online.wsj.com'+url
|
||||||
|
title = self.tag_to_string(a).strip()
|
||||||
|
if not title or title.startswith('['): continue
|
||||||
|
if title:
|
||||||
|
articles.append({'title':self.tag_to_string(a),
|
||||||
|
'url':url, 'description':'', 'date':''})
|
||||||
|
self.log('\t\tFound related:', title)
|
||||||
|
'''
|
||||||
|
|
||||||
|
return articles
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
||||||
|
|
||||||
|
|
||||||
|
BIN
resources/tracer.epub
Normal file
@ -436,7 +436,7 @@ from calibre.devices.blackberry.driver import BLACKBERRY
|
|||||||
from calibre.devices.cybook.driver import CYBOOK
|
from calibre.devices.cybook.driver import CYBOOK
|
||||||
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
|
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
|
||||||
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
|
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
|
||||||
BOOQ, ELONEX
|
BOOQ, ELONEX, POCKETBOOK301
|
||||||
from calibre.devices.iliad.driver import ILIAD
|
from calibre.devices.iliad.driver import ILIAD
|
||||||
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
||||||
from calibre.devices.jetbook.driver import JETBOOK
|
from calibre.devices.jetbook.driver import JETBOOK
|
||||||
@ -457,9 +457,12 @@ from calibre.devices.misc import PALMPRE, AVANT
|
|||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
|
|
||||||
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
|
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
|
||||||
|
LibraryThing
|
||||||
|
from calibre.ebooks.metadata.douban import DoubanBooks
|
||||||
from calibre.library.catalog import CSV_XML, EPUB_MOBI
|
from calibre.library.catalog import CSV_XML, EPUB_MOBI
|
||||||
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, CSV_XML, EPUB_MOBI]
|
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
|
||||||
|
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI]
|
||||||
plugins += [
|
plugins += [
|
||||||
ComicInput,
|
ComicInput,
|
||||||
EPUBInput,
|
EPUBInput,
|
||||||
@ -507,6 +510,7 @@ plugins += [
|
|||||||
JETBOOK,
|
JETBOOK,
|
||||||
SHINEBOOK,
|
SHINEBOOK,
|
||||||
POCKETBOOK360,
|
POCKETBOOK360,
|
||||||
|
POCKETBOOK301,
|
||||||
KINDLE,
|
KINDLE,
|
||||||
KINDLE2,
|
KINDLE2,
|
||||||
KINDLE_DX,
|
KINDLE_DX,
|
||||||
|
@ -21,7 +21,7 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
|||||||
platform = 'linux'
|
platform = 'linux'
|
||||||
if iswindows:
|
if iswindows:
|
||||||
platform = 'windows'
|
platform = 'windows'
|
||||||
if isosx:
|
elif isosx:
|
||||||
platform = 'osx'
|
platform = 'osx'
|
||||||
|
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -32,19 +32,25 @@ def _config():
|
|||||||
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
|
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
|
||||||
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
|
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
|
||||||
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
|
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
|
||||||
|
c.add_opt('enabled_plugins', default=set([]), help=_('Enabled plugins'))
|
||||||
|
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
|
||||||
|
|
||||||
class InvalidPlugin(ValueError):
|
class InvalidPlugin(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class PluginNotFound(ValueError):
|
class PluginNotFound(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def load_plugin(path_to_zip_file):
|
def find_plugin(name):
|
||||||
|
for plugin in _initialized_plugins:
|
||||||
|
if plugin.name == name:
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin(path_to_zip_file): # {{{
|
||||||
'''
|
'''
|
||||||
Load plugin from zip file or raise InvalidPlugin error
|
Load plugin from zip file or raise InvalidPlugin error
|
||||||
|
|
||||||
@ -76,11 +82,120 @@ def load_plugin(path_to_zip_file):
|
|||||||
|
|
||||||
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
|
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
|
||||||
|
|
||||||
_initialized_plugins = []
|
# }}}
|
||||||
|
|
||||||
|
# Enable/disable plugins {{{
|
||||||
|
|
||||||
|
def disable_plugin(plugin_or_name):
|
||||||
|
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||||
|
plugin = find_plugin(x)
|
||||||
|
if not plugin.can_be_disabled:
|
||||||
|
raise ValueError('Plugin %s cannot be disabled'%x)
|
||||||
|
dp = config['disabled_plugins']
|
||||||
|
dp.add(x)
|
||||||
|
config['disabled_plugins'] = dp
|
||||||
|
ep = config['enabled_plugins']
|
||||||
|
if x in ep:
|
||||||
|
ep.remove(x)
|
||||||
|
config['enabled_plugins'] = ep
|
||||||
|
|
||||||
|
def enable_plugin(plugin_or_name):
|
||||||
|
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||||
|
dp = config['disabled_plugins']
|
||||||
|
if x in dp:
|
||||||
|
dp.remove(x)
|
||||||
|
config['disabled_plugins'] = dp
|
||||||
|
ep = config['enabled_plugins']
|
||||||
|
ep.add(x)
|
||||||
|
config['enabled_plugins'] = ep
|
||||||
|
|
||||||
|
default_disabled_plugins = set([
|
||||||
|
'Douban Books',
|
||||||
|
])
|
||||||
|
|
||||||
|
def is_disabled(plugin):
|
||||||
|
if plugin.name in config['enabled_plugins']: return False
|
||||||
|
return plugin.name in config['disabled_plugins'] or \
|
||||||
|
plugin.name in default_disabled_plugins
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# File type plugins {{{
|
||||||
|
|
||||||
_on_import = {}
|
_on_import = {}
|
||||||
_on_preprocess = {}
|
_on_preprocess = {}
|
||||||
_on_postprocess = {}
|
_on_postprocess = {}
|
||||||
|
|
||||||
|
def reread_filetype_plugins():
|
||||||
|
global _on_import
|
||||||
|
global _on_preprocess
|
||||||
|
global _on_postprocess
|
||||||
|
_on_import = {}
|
||||||
|
_on_preprocess = {}
|
||||||
|
_on_postprocess = {}
|
||||||
|
|
||||||
|
for plugin in _initialized_plugins:
|
||||||
|
if isinstance(plugin, FileTypePlugin):
|
||||||
|
for ft in plugin.file_types:
|
||||||
|
if plugin.on_import:
|
||||||
|
if not _on_import.has_key(ft):
|
||||||
|
_on_import[ft] = []
|
||||||
|
_on_import[ft].append(plugin)
|
||||||
|
if plugin.on_preprocess:
|
||||||
|
if not _on_preprocess.has_key(ft):
|
||||||
|
_on_preprocess[ft] = []
|
||||||
|
_on_preprocess[ft].append(plugin)
|
||||||
|
if plugin.on_postprocess:
|
||||||
|
if not _on_postprocess.has_key(ft):
|
||||||
|
_on_postprocess[ft] = []
|
||||||
|
_on_postprocess[ft].append(plugin)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
||||||
|
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
|
||||||
|
'postprocess':_on_postprocess}[occasion]
|
||||||
|
customization = config['plugin_customization']
|
||||||
|
if ft is None:
|
||||||
|
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
|
||||||
|
nfp = path_to_file
|
||||||
|
for plugin in occasion.get(ft, []):
|
||||||
|
if is_disabled(plugin):
|
||||||
|
continue
|
||||||
|
plugin.site_customization = customization.get(plugin.name, '')
|
||||||
|
with plugin:
|
||||||
|
try:
|
||||||
|
nfp = plugin.run(path_to_file)
|
||||||
|
if not nfp:
|
||||||
|
nfp = path_to_file
|
||||||
|
except:
|
||||||
|
print 'Running file type plugin %s failed with traceback:'%plugin.name
|
||||||
|
traceback.print_exc()
|
||||||
|
x = lambda j : os.path.normpath(os.path.normcase(j))
|
||||||
|
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
|
||||||
|
shutil.copyfile(nfp, path_to_file)
|
||||||
|
nfp = path_to_file
|
||||||
|
return nfp
|
||||||
|
|
||||||
|
run_plugins_on_import = functools.partial(_run_filetype_plugins,
|
||||||
|
occasion='import')
|
||||||
|
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
|
||||||
|
occasion='preprocess')
|
||||||
|
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
|
||||||
|
occasion='postprocess')
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Plugin customization {{{
|
||||||
|
def customize_plugin(plugin, custom):
|
||||||
|
d = config['plugin_customization']
|
||||||
|
d[plugin.name] = custom.strip()
|
||||||
|
config['plugin_customization'] = d
|
||||||
|
|
||||||
|
def plugin_customization(plugin):
|
||||||
|
return config['plugin_customization'].get(plugin.name, '')
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
# Input/Output profiles {{{
|
||||||
def input_profiles():
|
def input_profiles():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, InputProfile):
|
if isinstance(plugin, InputProfile):
|
||||||
@ -90,7 +205,9 @@ def output_profiles():
|
|||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, OutputProfile):
|
if isinstance(plugin, OutputProfile):
|
||||||
yield plugin
|
yield plugin
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Metadata sources {{{
|
||||||
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
|
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, MetadataSource) and \
|
if isinstance(plugin, MetadataSource) and \
|
||||||
@ -117,31 +234,9 @@ def migrate_isbndb_key():
|
|||||||
if key:
|
if key:
|
||||||
prefs.set('isbndb_com_key', '')
|
prefs.set('isbndb_com_key', '')
|
||||||
set_isbndb_key(key)
|
set_isbndb_key(key)
|
||||||
|
# }}}
|
||||||
|
|
||||||
def reread_filetype_plugins():
|
# Metadata read/write {{{
|
||||||
global _on_import
|
|
||||||
global _on_preprocess
|
|
||||||
global _on_postprocess
|
|
||||||
_on_import = {}
|
|
||||||
_on_preprocess = {}
|
|
||||||
_on_postprocess = {}
|
|
||||||
|
|
||||||
for plugin in _initialized_plugins:
|
|
||||||
if isinstance(plugin, FileTypePlugin):
|
|
||||||
for ft in plugin.file_types:
|
|
||||||
if plugin.on_import:
|
|
||||||
if not _on_import.has_key(ft):
|
|
||||||
_on_import[ft] = []
|
|
||||||
_on_import[ft].append(plugin)
|
|
||||||
if plugin.on_preprocess:
|
|
||||||
if not _on_preprocess.has_key(ft):
|
|
||||||
_on_preprocess[ft] = []
|
|
||||||
_on_preprocess[ft].append(plugin)
|
|
||||||
if plugin.on_postprocess:
|
|
||||||
if not _on_postprocess.has_key(ft):
|
|
||||||
_on_postprocess[ft] = []
|
|
||||||
_on_postprocess[ft].append(plugin)
|
|
||||||
|
|
||||||
_metadata_readers = {}
|
_metadata_readers = {}
|
||||||
_metadata_writers = {}
|
_metadata_writers = {}
|
||||||
def reread_metadata_plugins():
|
def reread_metadata_plugins():
|
||||||
@ -233,51 +328,9 @@ def set_file_type_metadata(stream, mi, ftype):
|
|||||||
print 'Failed to set metadata for', repr(getattr(mi, 'title', ''))
|
print 'Failed to set metadata for', repr(getattr(mi, 'title', ''))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
# Add/remove plugins {{{
|
||||||
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
|
|
||||||
'postprocess':_on_postprocess}[occasion]
|
|
||||||
customization = config['plugin_customization']
|
|
||||||
if ft is None:
|
|
||||||
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
|
|
||||||
nfp = path_to_file
|
|
||||||
for plugin in occasion.get(ft, []):
|
|
||||||
if is_disabled(plugin):
|
|
||||||
continue
|
|
||||||
plugin.site_customization = customization.get(plugin.name, '')
|
|
||||||
with plugin:
|
|
||||||
try:
|
|
||||||
nfp = plugin.run(path_to_file)
|
|
||||||
if not nfp:
|
|
||||||
nfp = path_to_file
|
|
||||||
except:
|
|
||||||
print 'Running file type plugin %s failed with traceback:'%plugin.name
|
|
||||||
traceback.print_exc()
|
|
||||||
x = lambda j : os.path.normpath(os.path.normcase(j))
|
|
||||||
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
|
|
||||||
shutil.copyfile(nfp, path_to_file)
|
|
||||||
nfp = path_to_file
|
|
||||||
return nfp
|
|
||||||
|
|
||||||
run_plugins_on_import = functools.partial(_run_filetype_plugins,
|
|
||||||
occasion='import')
|
|
||||||
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
|
|
||||||
occasion='preprocess')
|
|
||||||
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
|
|
||||||
occasion='postprocess')
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_plugin(plugin, path_to_zip_file):
|
|
||||||
try:
|
|
||||||
p = plugin(path_to_zip_file)
|
|
||||||
p.initialize()
|
|
||||||
return p
|
|
||||||
except Exception:
|
|
||||||
print 'Failed to initialize plugin:', plugin.name, plugin.version
|
|
||||||
tb = traceback.format_exc()
|
|
||||||
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
|
|
||||||
%tb) + '\n'+tb)
|
|
||||||
|
|
||||||
|
|
||||||
def add_plugin(path_to_zip_file):
|
def add_plugin(path_to_zip_file):
|
||||||
make_config_dir()
|
make_config_dir()
|
||||||
@ -307,14 +360,9 @@ def remove_plugin(plugin_or_name):
|
|||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
def is_disabled(plugin):
|
# }}}
|
||||||
return plugin.name in config['disabled_plugins']
|
|
||||||
|
|
||||||
def find_plugin(name):
|
|
||||||
for plugin in _initialized_plugins:
|
|
||||||
if plugin.name == name:
|
|
||||||
return plugin
|
|
||||||
|
|
||||||
|
# Input/Output format plugins {{{
|
||||||
|
|
||||||
def input_format_plugins():
|
def input_format_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
@ -364,6 +412,9 @@ def available_output_formats():
|
|||||||
formats.add(plugin.file_type)
|
formats.add(plugin.file_type)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Catalog plugins {{{
|
||||||
|
|
||||||
def catalog_plugins():
|
def catalog_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
@ -383,27 +434,32 @@ def plugin_for_catalog_format(fmt):
|
|||||||
if fmt.lower() in plugin.file_types:
|
if fmt.lower() in plugin.file_types:
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
def device_plugins():
|
# }}}
|
||||||
|
|
||||||
|
def device_plugins(): # {{{
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, DevicePlugin):
|
if isinstance(plugin, DevicePlugin):
|
||||||
if not is_disabled(plugin):
|
if not is_disabled(plugin):
|
||||||
yield plugin
|
if platform in plugin.supported_platforms:
|
||||||
|
yield plugin
|
||||||
|
# }}}
|
||||||
|
|
||||||
def disable_plugin(plugin_or_name):
|
|
||||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
|
||||||
plugin = find_plugin(x)
|
|
||||||
if not plugin.can_be_disabled:
|
|
||||||
raise ValueError('Plugin %s cannot be disabled'%x)
|
|
||||||
dp = config['disabled_plugins']
|
|
||||||
dp.add(x)
|
|
||||||
config['disabled_plugins'] = dp
|
|
||||||
|
|
||||||
def enable_plugin(plugin_or_name):
|
# Initialize plugins {{{
|
||||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
|
||||||
dp = config['disabled_plugins']
|
_initialized_plugins = []
|
||||||
if x in dp:
|
|
||||||
dp.remove(x)
|
def initialize_plugin(plugin, path_to_zip_file):
|
||||||
config['disabled_plugins'] = dp
|
try:
|
||||||
|
p = plugin(path_to_zip_file)
|
||||||
|
p.initialize()
|
||||||
|
return p
|
||||||
|
except Exception:
|
||||||
|
print 'Failed to initialize plugin:', plugin.name, plugin.version
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
|
||||||
|
%tb) + '\n'+tb)
|
||||||
|
|
||||||
|
|
||||||
def initialize_plugins():
|
def initialize_plugins():
|
||||||
global _initialized_plugins
|
global _initialized_plugins
|
||||||
@ -425,10 +481,14 @@ def initialize_plugins():
|
|||||||
|
|
||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
|
|
||||||
def intialized_plugins():
|
def initialized_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
yield plugin
|
yield plugin
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# CLI {{{
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
parser = OptionParser(usage=_('''\
|
parser = OptionParser(usage=_('''\
|
||||||
%prog options
|
%prog options
|
||||||
@ -449,17 +509,6 @@ def option_parser():
|
|||||||
help=_('Disable the named plugin'))
|
help=_('Disable the named plugin'))
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def initialized_plugins():
|
|
||||||
return _initialized_plugins
|
|
||||||
|
|
||||||
def customize_plugin(plugin, custom):
|
|
||||||
d = config['plugin_customization']
|
|
||||||
d[plugin.name] = custom.strip()
|
|
||||||
config['plugin_customization'] = d
|
|
||||||
|
|
||||||
def plugin_customization(plugin):
|
|
||||||
return config['plugin_customization'].get(plugin.name, '')
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
@ -504,3 +553,5 @@ def main(args=sys.argv):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -201,4 +201,21 @@ class ELONEX(EB600):
|
|||||||
def can_handle(cls, dev, debug=False):
|
def can_handle(cls, dev, debug=False):
|
||||||
return dev[3] == 'Elonex' and dev[4] == 'eBook'
|
return dev[3] == 'Elonex' and dev[4] == 'eBook'
|
||||||
|
|
||||||
|
class POCKETBOOK301(USBMS):
|
||||||
|
|
||||||
|
name = 'PocketBook 301 Device Interface'
|
||||||
|
description = _('Communicate with the PocketBook 301 reader.')
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', 'txt']
|
||||||
|
|
||||||
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
|
MAIN_MEMORY_VOLUME_LABEL = 'PocketBook 301 Main Memory'
|
||||||
|
STORAGE_CARD_VOLUME_LABEL = 'PocketBook 301 Storage Card'
|
||||||
|
|
||||||
|
VENDOR_ID = [0x1]
|
||||||
|
PRODUCT_ID = [0x301]
|
||||||
|
BCD = [0x132]
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,9 +81,6 @@ class HANLINV3(USBMS):
|
|||||||
return drives
|
return drives
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HANLINV5(HANLINV3):
|
class HANLINV5(HANLINV3):
|
||||||
name = 'Hanlin V5 driver'
|
name = 'Hanlin V5 driver'
|
||||||
gui_name = 'Hanlin V5'
|
gui_name = 'Hanlin V5'
|
||||||
@ -120,8 +117,22 @@ class BOOX(HANLINV3):
|
|||||||
MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory'
|
MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory'
|
||||||
STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card'
|
STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card'
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = 'MyBooks'
|
EBOOK_DIR_MAIN = ['MyBooks']
|
||||||
EBOOK_DIR_CARD_A = 'MyBooks'
|
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
|
||||||
|
'send e-books to on the device. The first one that exists will '
|
||||||
|
'be used.')
|
||||||
|
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
|
||||||
|
|
||||||
|
# EBOOK_DIR_CARD_A = 'MyBooks' ## Am quite sure we need this.
|
||||||
|
|
||||||
|
def post_open_callback(self):
|
||||||
|
opts = self.settings()
|
||||||
|
dirs = opts.extra_customization
|
||||||
|
if not dirs:
|
||||||
|
dirs = self.EBOOK_DIR_MAIN
|
||||||
|
else:
|
||||||
|
dirs = [x.strip() for x in dirs.split(',')]
|
||||||
|
self.EBOOK_DIR_MAIN = dirs
|
||||||
|
|
||||||
def windows_sort_drives(self, drives):
|
def windows_sort_drives(self, drives):
|
||||||
return drives
|
return drives
|
||||||
|
@ -55,6 +55,7 @@ class PRS505(USBMS):
|
|||||||
|
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
MUST_READ_METADATA = True
|
MUST_READ_METADATA = True
|
||||||
|
SUPPORTS_USE_AUTHOR_SORT = True
|
||||||
EBOOK_DIR_MAIN = 'database/media/books'
|
EBOOK_DIR_MAIN = 'database/media/books'
|
||||||
|
|
||||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
||||||
@ -125,7 +126,7 @@ class PRS505(USBMS):
|
|||||||
d = os.path.dirname(paths[source_id])
|
d = os.path.dirname(paths[source_id])
|
||||||
if not os.path.exists(d):
|
if not os.path.exists(d):
|
||||||
os.makedirs(d)
|
os.makedirs(d)
|
||||||
return XMLCache(paths, prefixes)
|
return XMLCache(paths, prefixes, self.settings().use_author_sort)
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
debug_print('PRS505: starting fetching books for card', oncard)
|
debug_print('PRS505: starting fetching books for card', oncard)
|
||||||
|
@ -60,12 +60,13 @@ def uuid():
|
|||||||
|
|
||||||
class XMLCache(object):
|
class XMLCache(object):
|
||||||
|
|
||||||
def __init__(self, paths, prefixes):
|
def __init__(self, paths, prefixes, use_author_sort):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
debug_print('Building XMLCache...')
|
debug_print('Building XMLCache...')
|
||||||
pprint(paths)
|
pprint(paths)
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.prefixes = prefixes
|
self.prefixes = prefixes
|
||||||
|
self.use_author_sort = use_author_sort
|
||||||
|
|
||||||
# Parse XML files {{{
|
# Parse XML files {{{
|
||||||
parser = etree.XMLParser(recover=True)
|
parser = etree.XMLParser(recover=True)
|
||||||
@ -434,7 +435,10 @@ class XMLCache(object):
|
|||||||
if not ts:
|
if not ts:
|
||||||
ts = title_sort(title)
|
ts = title_sort(title)
|
||||||
record.set('titleSorter', ts)
|
record.set('titleSorter', ts)
|
||||||
record.set('author', authors_to_string(book.authors))
|
if self.use_author_sort and book.author_sort is not None:
|
||||||
|
record.set('author', book.author_sort)
|
||||||
|
else:
|
||||||
|
record.set('author', authors_to_string(book.authors))
|
||||||
ext = os.path.splitext(path)[1]
|
ext = os.path.splitext(path)[1]
|
||||||
if ext:
|
if ext:
|
||||||
ext = ext[1:].lower()
|
ext = ext[1:].lower()
|
||||||
|
@ -80,6 +80,7 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
|
|
||||||
SUPPORTS_SUB_DIRS = False
|
SUPPORTS_SUB_DIRS = False
|
||||||
MUST_READ_METADATA = False
|
MUST_READ_METADATA = False
|
||||||
|
SUPPORTS_USE_AUTHOR_SORT = False
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = ''
|
EBOOK_DIR_MAIN = ''
|
||||||
EBOOK_DIR_CARD_A = ''
|
EBOOK_DIR_CARD_A = ''
|
||||||
|
@ -32,6 +32,8 @@ class DeviceConfig(object):
|
|||||||
help=_('Place files in sub directories if the device supports them'))
|
help=_('Place files in sub directories if the device supports them'))
|
||||||
c.add_opt('read_metadata', default=True,
|
c.add_opt('read_metadata', default=True,
|
||||||
help=_('Read metadata from files on device'))
|
help=_('Read metadata from files on device'))
|
||||||
|
c.add_opt('use_author_sort', default=False,
|
||||||
|
help=_('Use author sort instead of author'))
|
||||||
c.add_opt('save_template', default=cls._default_save_template(),
|
c.add_opt('save_template', default=cls._default_save_template(),
|
||||||
help=_('Template to control how books are saved'))
|
help=_('Template to control how books are saved'))
|
||||||
c.add_opt('extra_customization',
|
c.add_opt('extra_customization',
|
||||||
@ -47,7 +49,8 @@ class DeviceConfig(object):
|
|||||||
def config_widget(cls):
|
def config_widget(cls):
|
||||||
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
||||||
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
||||||
cls.MUST_READ_METADATA, cls.EXTRA_CUSTOMIZATION_MESSAGE)
|
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
|
||||||
|
cls.EXTRA_CUSTOMIZATION_MESSAGE)
|
||||||
return cw
|
return cw
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -58,6 +61,8 @@ class DeviceConfig(object):
|
|||||||
proxy['use_subdirs'] = config_widget.use_subdirs()
|
proxy['use_subdirs'] = config_widget.use_subdirs()
|
||||||
if not cls.MUST_READ_METADATA:
|
if not cls.MUST_READ_METADATA:
|
||||||
proxy['read_metadata'] = config_widget.read_metadata()
|
proxy['read_metadata'] = config_widget.read_metadata()
|
||||||
|
if cls.SUPPORTS_USE_AUTHOR_SORT:
|
||||||
|
proxy['use_author_sort'] = config_widget.use_author_sort()
|
||||||
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
|
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
|
||||||
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
||||||
if not ec:
|
if not ec:
|
||||||
|
@ -299,7 +299,7 @@ class USBMS(CLI, Device):
|
|||||||
def replfunc(match):
|
def replfunc(match):
|
||||||
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
||||||
return '(?P<' + match.group(1) + '>.+?)'
|
return '(?P<' + match.group(1) + '>.+?)'
|
||||||
elif match.group(1) == 'authors':
|
elif match.group(1) in ['authors', 'author_sort']:
|
||||||
return '(?P<author>.+?)'
|
return '(?P<author>.+?)'
|
||||||
else:
|
else:
|
||||||
return '(.+?)'
|
return '(.+?)'
|
||||||
|
@ -8,7 +8,7 @@ import os, re
|
|||||||
from mimetypes import guess_type as guess_mimetype
|
from mimetypes import guess_type as guess_mimetype
|
||||||
|
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
|
||||||
|
from calibre.constants import iswindows
|
||||||
from calibre.utils.chm.chm import CHMFile
|
from calibre.utils.chm.chm import CHMFile
|
||||||
from calibre.utils.chm.chmlib import (
|
from calibre.utils.chm.chmlib import (
|
||||||
CHM_RESOLVE_SUCCESS, CHM_ENUMERATE_NORMAL,
|
CHM_RESOLVE_SUCCESS, CHM_ENUMERATE_NORMAL,
|
||||||
@ -135,10 +135,16 @@ class CHMReader(CHMFile):
|
|||||||
if lpath.find(';') != -1:
|
if lpath.find(';') != -1:
|
||||||
# fix file names with ";<junk>" at the end, see _reformat()
|
# fix file names with ";<junk>" at the end, see _reformat()
|
||||||
lpath = lpath.split(';')[0]
|
lpath = lpath.split(';')[0]
|
||||||
with open(lpath, 'wb') as f:
|
try:
|
||||||
if guess_mimetype(path)[0] == ('text/html'):
|
with open(lpath, 'wb') as f:
|
||||||
data = self._reformat(data)
|
if guess_mimetype(path)[0] == ('text/html'):
|
||||||
f.write(data)
|
data = self._reformat(data)
|
||||||
|
f.write(data)
|
||||||
|
except:
|
||||||
|
if iswindows and len(lpath) > 250:
|
||||||
|
self.log.warn('%r filename too long, skipping'%path)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
self._extracted = True
|
self._extracted = True
|
||||||
files = os.listdir(output_dir)
|
files = os.listdir(output_dir)
|
||||||
if self.hhc_path not in files:
|
if self.hhc_path not in files:
|
||||||
|
@ -385,14 +385,6 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
if val and not pval:
|
if val and not pval:
|
||||||
rule.style.setProperty('padding-left', val)
|
rule.style.setProperty('padding-left', val)
|
||||||
|
|
||||||
if stylesheet is not None:
|
|
||||||
stylesheet.data.add('a { color: inherit; text-decoration: inherit; '
|
|
||||||
'cursor: default; }')
|
|
||||||
stylesheet.data.add('a[href] { color: blue; '
|
|
||||||
'text-decoration: underline; cursor:pointer; }')
|
|
||||||
else:
|
|
||||||
self.oeb.log.warn('No stylesheet found')
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def workaround_sony_quirks(self): # {{{
|
def workaround_sony_quirks(self): # {{{
|
||||||
|
@ -28,10 +28,14 @@ def authors_to_string(authors):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
_bracket_pat = re.compile(r'[\[({].*?[})\]]')
|
||||||
def author_to_author_sort(author):
|
def author_to_author_sort(author):
|
||||||
|
if not author:
|
||||||
|
return ''
|
||||||
method = tweaks['author_sort_copy_method']
|
method = tweaks['author_sort_copy_method']
|
||||||
if method == 'copy' or (method == 'comma' and ',' in author):
|
if method == 'copy' or (method == 'comma' and ',' in author):
|
||||||
return author
|
return author
|
||||||
|
author = _bracket_pat.sub('', author).strip()
|
||||||
tokens = author.split()
|
tokens = author.split()
|
||||||
tokens = tokens[-1:] + tokens[:-1]
|
tokens = tokens[-1:] + tokens[:-1]
|
||||||
if len(tokens) > 1:
|
if len(tokens) > 1:
|
||||||
@ -256,7 +260,7 @@ class MetaInformation(object):
|
|||||||
setattr(self, x, getattr(mi, x, None))
|
setattr(self, x, getattr(mi, x, None))
|
||||||
|
|
||||||
def print_all_attributes(self):
|
def print_all_attributes(self):
|
||||||
for x in ('author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
for x in ('title','author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
||||||
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
||||||
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
||||||
|
258
src/calibre/ebooks/metadata/douban.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>; 2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import sys, textwrap
|
||||||
|
import traceback
|
||||||
|
from urllib import urlencode
|
||||||
|
from functools import partial
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from calibre import browser, preferred_encoding
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
from calibre.utils.config import OptionParser
|
||||||
|
from calibre.ebooks.metadata.fetch import MetadataSource
|
||||||
|
from calibre.utils.date import parse_date, utcnow
|
||||||
|
|
||||||
|
DOUBAN_API_KEY = None
|
||||||
|
NAMESPACES = {
|
||||||
|
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
|
||||||
|
'atom' : 'http://www.w3.org/2005/Atom',
|
||||||
|
'db': 'http://www.douban.com/xmlns/'
|
||||||
|
}
|
||||||
|
XPath = partial(etree.XPath, namespaces=NAMESPACES)
|
||||||
|
total_results = XPath('//openSearch:totalResults')
|
||||||
|
start_index = XPath('//openSearch:startIndex')
|
||||||
|
items_per_page = XPath('//openSearch:itemsPerPage')
|
||||||
|
entry = XPath('//atom:entry')
|
||||||
|
entry_id = XPath('descendant::atom:id')
|
||||||
|
title = XPath('descendant::atom:title')
|
||||||
|
description = XPath('descendant::atom:summary')
|
||||||
|
publisher = XPath("descendant::db:attribute[@name='publisher']")
|
||||||
|
isbn = XPath("descendant::db:attribute[@name='isbn13']")
|
||||||
|
date = XPath("descendant::db:attribute[@name='pubdate']")
|
||||||
|
creator = XPath("descendant::db:attribute[@name='author']")
|
||||||
|
tag = XPath("descendant::db:tag")
|
||||||
|
|
||||||
|
class DoubanBooks(MetadataSource):
|
||||||
|
|
||||||
|
name = 'Douban Books'
|
||||||
|
description = _('Downloads metadata from Douban.com')
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
|
||||||
|
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
|
||||||
|
version = (1, 0, 0) # The version number of this plugin
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
try:
|
||||||
|
self.results = search(self.title, self.book_author, self.publisher,
|
||||||
|
self.isbn, max_results=10,
|
||||||
|
verbose=self.verbose)
|
||||||
|
except Exception, e:
|
||||||
|
self.exception = e
|
||||||
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
def report(verbose):
|
||||||
|
if verbose:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
class Query(object):
|
||||||
|
|
||||||
|
SEARCH_URL = 'http://api.douban.com/book/subjects?'
|
||||||
|
ISBN_URL = 'http://api.douban.com/book/subject/isbn/'
|
||||||
|
|
||||||
|
type = "search"
|
||||||
|
|
||||||
|
def __init__(self, title=None, author=None, publisher=None, isbn=None,
|
||||||
|
max_results=20, start_index=1):
|
||||||
|
assert not(title is None and author is None and publisher is None and \
|
||||||
|
isbn is None)
|
||||||
|
assert (int(max_results) < 21)
|
||||||
|
q = ''
|
||||||
|
if isbn is not None:
|
||||||
|
q = isbn
|
||||||
|
self.type = 'isbn'
|
||||||
|
else:
|
||||||
|
def build_term(parts):
|
||||||
|
return ' '.join(x for x in parts)
|
||||||
|
if title is not None:
|
||||||
|
q += build_term(title.split())
|
||||||
|
if author is not None:
|
||||||
|
q += (' ' if q else '') + build_term(author.split())
|
||||||
|
if publisher is not None:
|
||||||
|
q += (' ' if q else '') + build_term(publisher.split())
|
||||||
|
self.type = 'search'
|
||||||
|
|
||||||
|
if isinstance(q, unicode):
|
||||||
|
q = q.encode('utf-8')
|
||||||
|
|
||||||
|
if self.type == "isbn":
|
||||||
|
self.url = self.ISBN_URL + q
|
||||||
|
if DOUBAN_API_KEY is not None:
|
||||||
|
self.url = self.url + "?apikey=" + DOUBAN_API_KEY
|
||||||
|
else:
|
||||||
|
self.url = self.SEARCH_URL+urlencode({
|
||||||
|
'q':q,
|
||||||
|
'max-results':max_results,
|
||||||
|
'start-index':start_index,
|
||||||
|
})
|
||||||
|
if DOUBAN_API_KEY is not None:
|
||||||
|
self.url = self.url + "&apikey=" + DOUBAN_API_KEY
|
||||||
|
|
||||||
|
def __call__(self, browser, verbose):
|
||||||
|
if verbose:
|
||||||
|
print 'Query:', self.url
|
||||||
|
if self.type == "search":
|
||||||
|
feed = etree.fromstring(browser.open(self.url).read())
|
||||||
|
total = int(total_results(feed)[0].text)
|
||||||
|
start = int(start_index(feed)[0].text)
|
||||||
|
entries = entry(feed)
|
||||||
|
new_start = start + len(entries)
|
||||||
|
if new_start > total:
|
||||||
|
new_start = 0
|
||||||
|
return entries, new_start
|
||||||
|
elif self.type == "isbn":
|
||||||
|
feed = etree.fromstring(browser.open(self.url).read())
|
||||||
|
entries = entry(feed)
|
||||||
|
return entries, 0
|
||||||
|
|
||||||
|
class ResultList(list):
|
||||||
|
|
||||||
|
def get_description(self, entry, verbose):
|
||||||
|
try:
|
||||||
|
desc = description(entry)
|
||||||
|
if desc:
|
||||||
|
return 'SUMMARY:\n'+desc[0].text
|
||||||
|
except:
|
||||||
|
report(verbose)
|
||||||
|
|
||||||
|
def get_title(self, entry):
|
||||||
|
candidates = [x.text for x in title(entry)]
|
||||||
|
return ': '.join(candidates)
|
||||||
|
|
||||||
|
def get_authors(self, entry):
|
||||||
|
m = creator(entry)
|
||||||
|
if not m:
|
||||||
|
m = []
|
||||||
|
m = [x.text for x in m]
|
||||||
|
return m
|
||||||
|
|
||||||
|
def get_tags(self, entry, verbose):
|
||||||
|
try:
|
||||||
|
btags = [x.attrib["name"] for x in tag(entry)]
|
||||||
|
tags = []
|
||||||
|
for t in btags:
|
||||||
|
tags.extend([y.strip() for y in t.split('/')])
|
||||||
|
tags = list(sorted(list(set(tags))))
|
||||||
|
except:
|
||||||
|
report(verbose)
|
||||||
|
tags = []
|
||||||
|
return [x.replace(',', ';') for x in tags]
|
||||||
|
|
||||||
|
def get_publisher(self, entry, verbose):
|
||||||
|
try:
|
||||||
|
pub = publisher(entry)[0].text
|
||||||
|
except:
|
||||||
|
pub = None
|
||||||
|
return pub
|
||||||
|
|
||||||
|
def get_isbn(self, entry, verbose):
|
||||||
|
try:
|
||||||
|
isbn13 = isbn(entry)[0].text
|
||||||
|
except Exception:
|
||||||
|
isbn13 = None
|
||||||
|
return isbn13
|
||||||
|
|
||||||
|
def get_date(self, entry, verbose):
|
||||||
|
try:
|
||||||
|
d = date(entry)
|
||||||
|
if d:
|
||||||
|
default = utcnow().replace(day=15)
|
||||||
|
d = parse_date(d[0].text, assume_utc=True, default=default)
|
||||||
|
else:
|
||||||
|
d = None
|
||||||
|
except:
|
||||||
|
report(verbose)
|
||||||
|
d = None
|
||||||
|
return d
|
||||||
|
|
||||||
|
def populate(self, entries, browser, verbose=False):
|
||||||
|
for x in entries:
|
||||||
|
try:
|
||||||
|
id_url = entry_id(x)[0].text
|
||||||
|
title = self.get_title(x)
|
||||||
|
except:
|
||||||
|
report(verbose)
|
||||||
|
mi = MetaInformation(title, self.get_authors(x))
|
||||||
|
try:
|
||||||
|
if DOUBAN_API_KEY is not None:
|
||||||
|
id_url = id_url + "?apikey=" + DOUBAN_API_KEY
|
||||||
|
raw = browser.open(id_url).read()
|
||||||
|
feed = etree.fromstring(raw)
|
||||||
|
x = entry(feed)[0]
|
||||||
|
except Exception, e:
|
||||||
|
if verbose:
|
||||||
|
print 'Failed to get all details for an entry'
|
||||||
|
print e
|
||||||
|
mi.comments = self.get_description(x, verbose)
|
||||||
|
mi.tags = self.get_tags(x, verbose)
|
||||||
|
mi.isbn = self.get_isbn(x, verbose)
|
||||||
|
mi.publisher = self.get_publisher(x, verbose)
|
||||||
|
mi.pubdate = self.get_date(x, verbose)
|
||||||
|
self.append(mi)
|
||||||
|
|
||||||
|
def search(title=None, author=None, publisher=None, isbn=None,
|
||||||
|
verbose=False, max_results=40):
|
||||||
|
br = browser()
|
||||||
|
start, entries = 1, []
|
||||||
|
while start > 0 and len(entries) <= max_results:
|
||||||
|
new, start = Query(title=title, author=author, publisher=publisher,
|
||||||
|
isbn=isbn, max_results=max_results, start_index=start)(br, verbose)
|
||||||
|
if not new:
|
||||||
|
break
|
||||||
|
entries.extend(new)
|
||||||
|
|
||||||
|
entries = entries[:max_results]
|
||||||
|
|
||||||
|
ans = ResultList()
|
||||||
|
ans.populate(entries, br, verbose)
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def option_parser():
|
||||||
|
parser = OptionParser(textwrap.dedent(
|
||||||
|
'''\
|
||||||
|
%prog [options]
|
||||||
|
|
||||||
|
Fetch book metadata from Douban. You must specify one of title, author,
|
||||||
|
publisher or ISBN. If you specify ISBN the others are ignored. Will
|
||||||
|
fetch a maximum of 100 matches, so you should make your query as
|
||||||
|
specific as possible.
|
||||||
|
'''
|
||||||
|
))
|
||||||
|
parser.add_option('-t', '--title', help='Book title')
|
||||||
|
parser.add_option('-a', '--author', help='Book author(s)')
|
||||||
|
parser.add_option('-p', '--publisher', help='Book publisher')
|
||||||
|
parser.add_option('-i', '--isbn', help='Book ISBN')
|
||||||
|
parser.add_option('-m', '--max-results', default=10,
|
||||||
|
help='Maximum number of results to fetch')
|
||||||
|
parser.add_option('-v', '--verbose', default=0, action='count',
|
||||||
|
help='Be more verbose about errors')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def main(args=sys.argv):
|
||||||
|
parser = option_parser()
|
||||||
|
opts, args = parser.parse_args(args)
|
||||||
|
try:
|
||||||
|
results = search(opts.title, opts.author, opts.publisher, opts.isbn,
|
||||||
|
verbose=opts.verbose, max_results=int(opts.max_results))
|
||||||
|
except AssertionError:
|
||||||
|
report(True)
|
||||||
|
parser.print_help()
|
||||||
|
return 1
|
||||||
|
for result in results:
|
||||||
|
print unicode(result).encode(preferred_encoding)
|
||||||
|
print
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
@ -182,7 +182,7 @@ def get_metadata(stream, extract_cover=True):
|
|||||||
def get_quick_metadata(stream):
|
def get_quick_metadata(stream):
|
||||||
return get_metadata(stream, False)
|
return get_metadata(stream, False)
|
||||||
|
|
||||||
def set_metadata(stream, mi, apply_null=False):
|
def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
reader = OCFZipReader(stream, root=os.getcwdu())
|
reader = OCFZipReader(stream, root=os.getcwdu())
|
||||||
mi = MetaInformation(mi)
|
mi = MetaInformation(mi)
|
||||||
@ -196,6 +196,8 @@ def set_metadata(stream, mi, apply_null=False):
|
|||||||
reader.opf.tags = []
|
reader.opf.tags = []
|
||||||
if not getattr(mi, 'isbn', None):
|
if not getattr(mi, 'isbn', None):
|
||||||
reader.opf.isbn = None
|
reader.opf.isbn = None
|
||||||
|
if update_timestamp and mi.timestamp is not None:
|
||||||
|
reader.opf.timestamp = mi.timestamp
|
||||||
|
|
||||||
newopf = StringIO(reader.opf.render())
|
newopf = StringIO(reader.opf.render())
|
||||||
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
|
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
|
||||||
|
@ -198,6 +198,38 @@ class Amazon(MetadataSource):
|
|||||||
self.exception = e
|
self.exception = e
|
||||||
self.tb = traceback.format_exc()
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
class LibraryThing(MetadataSource):
|
||||||
|
|
||||||
|
name = 'LibraryThing'
|
||||||
|
metadata_type = 'social'
|
||||||
|
description = _('Downloads series information from librarything.com')
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
if not self.isbn:
|
||||||
|
return
|
||||||
|
from calibre import browser
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
import json
|
||||||
|
br = browser()
|
||||||
|
try:
|
||||||
|
raw = br.open(
|
||||||
|
'http://status.calibre-ebook.com/library_thing/metadata/'+self.isbn
|
||||||
|
).read()
|
||||||
|
data = json.loads(raw)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
if 'error' in data:
|
||||||
|
raise Exception(data['error'])
|
||||||
|
if 'series' in data and 'series_index' in data:
|
||||||
|
mi = MetaInformation(self.title, [])
|
||||||
|
mi.series = data['series']
|
||||||
|
mi.series_index = data['series_index']
|
||||||
|
self.results = mi
|
||||||
|
except Exception, e:
|
||||||
|
self.exception = e
|
||||||
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
|
||||||
def result_index(source, result):
|
def result_index(source, result):
|
||||||
if not result.isbn:
|
if not result.isbn:
|
||||||
return -1
|
return -1
|
||||||
@ -266,7 +298,7 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
with MetadataSources(fetchers) as manager:
|
with MetadataSources(fetchers) as manager:
|
||||||
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
|
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
|
||||||
manager.join()
|
manager.join()
|
||||||
ratings, tags, comments = [], set([]), set([])
|
ratings, tags, comments, series, series_index = [], set([]), set([]), None, None
|
||||||
for fetcher in fetchers:
|
for fetcher in fetchers:
|
||||||
if fetcher.results:
|
if fetcher.results:
|
||||||
dmi = fetcher.results
|
dmi = fetcher.results
|
||||||
@ -279,6 +311,10 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
mi.pubdate = dmi.pubdate
|
mi.pubdate = dmi.pubdate
|
||||||
if dmi.comments:
|
if dmi.comments:
|
||||||
comments.add(dmi.comments)
|
comments.add(dmi.comments)
|
||||||
|
if dmi.series is not None:
|
||||||
|
series = dmi.series
|
||||||
|
if dmi.series_index is not None:
|
||||||
|
series_index = dmi.series_index
|
||||||
if ratings:
|
if ratings:
|
||||||
rating = sum(ratings)/float(len(ratings))
|
rating = sum(ratings)/float(len(ratings))
|
||||||
if mi.rating is None or mi.rating < 0.1:
|
if mi.rating is None or mi.rating < 0.1:
|
||||||
@ -295,6 +331,9 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
mi.comments = ''
|
mi.comments = ''
|
||||||
for x in comments:
|
for x in comments:
|
||||||
mi.comments += x+'\n\n'
|
mi.comments += x+'\n\n'
|
||||||
|
if series and series_index is not None:
|
||||||
|
mi.series = series
|
||||||
|
mi.series_index = series_index
|
||||||
|
|
||||||
return [(x.name, x.exception, x.tb) for x in fetchers if x.exception is not
|
return [(x.name, x.exception, x.tb) for x in fetchers if x.exception is not
|
||||||
None]
|
None]
|
||||||
|
@ -736,7 +736,9 @@ class OPF(object):
|
|||||||
def fget(self):
|
def fget(self):
|
||||||
ans = []
|
ans = []
|
||||||
for tag in self.tags_path(self.metadata):
|
for tag in self.tags_path(self.metadata):
|
||||||
ans.append(self.get_text(tag))
|
text = self.get_text(tag)
|
||||||
|
if text and text.strip():
|
||||||
|
ans.extend([x.strip() for x in text.split(',')])
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
|
@ -61,6 +61,7 @@ class FormatState(object):
|
|||||||
self.italic = False
|
self.italic = False
|
||||||
self.bold = False
|
self.bold = False
|
||||||
self.strikethrough = False
|
self.strikethrough = False
|
||||||
|
self.underline = False
|
||||||
self.preserve = False
|
self.preserve = False
|
||||||
self.family = 'serif'
|
self.family = 'serif'
|
||||||
self.bgcolor = 'transparent'
|
self.bgcolor = 'transparent'
|
||||||
@ -79,7 +80,8 @@ class FormatState(object):
|
|||||||
and self.family == other.family \
|
and self.family == other.family \
|
||||||
and self.bgcolor == other.bgcolor \
|
and self.bgcolor == other.bgcolor \
|
||||||
and self.fgcolor == other.fgcolor \
|
and self.fgcolor == other.fgcolor \
|
||||||
and self.strikethrough == other.strikethrough
|
and self.strikethrough == other.strikethrough \
|
||||||
|
and self.underline == other.underline
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@ -251,6 +253,8 @@ class MobiMLizer(object):
|
|||||||
color=unicode(istate.fgcolor))
|
color=unicode(istate.fgcolor))
|
||||||
if istate.strikethrough:
|
if istate.strikethrough:
|
||||||
inline = etree.SubElement(inline, XHTML('s'))
|
inline = etree.SubElement(inline, XHTML('s'))
|
||||||
|
if istate.underline:
|
||||||
|
inline = etree.SubElement(inline, XHTML('u'))
|
||||||
bstate.inline = inline
|
bstate.inline = inline
|
||||||
bstate.istate = istate
|
bstate.istate = istate
|
||||||
inline = bstate.inline
|
inline = bstate.inline
|
||||||
@ -330,6 +334,7 @@ class MobiMLizer(object):
|
|||||||
istate.bgcolor = style['background-color']
|
istate.bgcolor = style['background-color']
|
||||||
istate.fgcolor = style['color']
|
istate.fgcolor = style['color']
|
||||||
istate.strikethrough = style['text-decoration'] == 'line-through'
|
istate.strikethrough = style['text-decoration'] == 'line-through'
|
||||||
|
istate.underline = style['text-decoration'] == 'underline'
|
||||||
if 'monospace' in style['font-family']:
|
if 'monospace' in style['font-family']:
|
||||||
istate.family = 'monospace'
|
istate.family = 'monospace'
|
||||||
elif 'sans-serif' in style['font-family']:
|
elif 'sans-serif' in style['font-family']:
|
||||||
|
@ -28,6 +28,7 @@ from calibre.constants import preferred_encoding, filesystem_encoding, \
|
|||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
|
||||||
|
|
||||||
class AnnotationsAction(object): # {{{
|
class AnnotationsAction(object): # {{{
|
||||||
|
|
||||||
@ -471,6 +472,45 @@ class DeleteAction(object): # {{{
|
|||||||
if ids:
|
if ids:
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def remove_matching_books_from_device(self, *args):
|
||||||
|
if not self.device_manager.is_device_connected:
|
||||||
|
d = error_dialog(self, _('Cannot delete books'),
|
||||||
|
_('No device is connected'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
ids = self._get_selected_ids()
|
||||||
|
if not ids:
|
||||||
|
#_get_selected_ids shows a dialog box if nothing is selected, so we
|
||||||
|
#do not need to show one here
|
||||||
|
return
|
||||||
|
to_delete = {}
|
||||||
|
some_to_delete = False
|
||||||
|
for model,name in ((self.memory_view.model(), _('Main memory')),
|
||||||
|
(self.card_a_view.model(), _('Storage Card A')),
|
||||||
|
(self.card_b_view.model(), _('Storage Card B'))):
|
||||||
|
to_delete[name] = (model, model.paths_for_db_ids(ids))
|
||||||
|
if len(to_delete[name][1]) > 0:
|
||||||
|
some_to_delete = True
|
||||||
|
if not some_to_delete:
|
||||||
|
d = error_dialog(self, _('No books to delete'),
|
||||||
|
_('None of the selected books are on the device'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
d = DeleteMatchingFromDeviceDialog(self, to_delete)
|
||||||
|
if d.exec_():
|
||||||
|
paths = {}
|
||||||
|
ids = {}
|
||||||
|
for (model, id, path) in d.result:
|
||||||
|
if model not in paths:
|
||||||
|
paths[model] = []
|
||||||
|
ids[model] = []
|
||||||
|
paths[model].append(path)
|
||||||
|
ids[model].append(id)
|
||||||
|
for model in paths:
|
||||||
|
job = self.remove_paths(paths[model])
|
||||||
|
self.delete_memory[job] = (paths[model], model)
|
||||||
|
model.mark_for_deletion(job, ids[model], rows_are_ids=True)
|
||||||
|
self.status_bar.show_message(_('Deleting books from device.'), 1000)
|
||||||
|
|
||||||
def delete_covers(self, *args):
|
def delete_covers(self, *args):
|
||||||
ids = self._get_selected_ids()
|
ids = self._get_selected_ids()
|
||||||
|
@ -62,11 +62,13 @@ def render_rows(data):
|
|||||||
class CoverView(QWidget): # {{{
|
class CoverView(QWidget): # {{{
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, vertical, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
self.setMaximumSize(QSize(120, 120))
|
self.setMaximumSize(QSize(120, 120))
|
||||||
self.setMinimumSize(QSize(120, 1))
|
self.setMinimumSize(QSize(120 if vertical else 20, 120 if vertical else
|
||||||
|
20))
|
||||||
self._current_pixmap_size = self.maximumSize()
|
self._current_pixmap_size = self.maximumSize()
|
||||||
|
self.vertical = vertical
|
||||||
|
|
||||||
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
|
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
|
||||||
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
|
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
|
||||||
@ -74,7 +76,8 @@ class CoverView(QWidget): # {{{
|
|||||||
self.animation.setStartValue(QSize(0, 0))
|
self.animation.setStartValue(QSize(0, 0))
|
||||||
self.animation.valueChanged.connect(self.value_changed)
|
self.animation.valueChanged.connect(self.value_changed)
|
||||||
|
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.setSizePolicy(QSizePolicy.Expanding if vertical else
|
||||||
|
QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||||
|
|
||||||
self.default_pixmap = QPixmap(I('book.svg'))
|
self.default_pixmap = QPixmap(I('book.svg'))
|
||||||
self.pixmap = self.default_pixmap
|
self.pixmap = self.default_pixmap
|
||||||
@ -98,8 +101,12 @@ class CoverView(QWidget): # {{{
|
|||||||
self.animation.setEndValue(self.current_pixmap_size)
|
self.animation.setEndValue(self.current_pixmap_size)
|
||||||
|
|
||||||
def relayout(self, parent_size):
|
def relayout(self, parent_size):
|
||||||
self.setMaximumSize(parent_size.width(),
|
if self.vertical:
|
||||||
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
|
self.setMaximumSize(parent_size.width(),
|
||||||
|
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
|
||||||
|
else:
|
||||||
|
self.setMaximumSize(1+int(3/4. * parent_size.height()),
|
||||||
|
parent_size.height())
|
||||||
self.resize(self.maximumSize())
|
self.resize(self.maximumSize())
|
||||||
self.animation.stop()
|
self.animation.stop()
|
||||||
self.do_layout()
|
self.do_layout()
|
||||||
@ -109,8 +116,7 @@ class CoverView(QWidget): # {{{
|
|||||||
|
|
||||||
def show_data(self, data):
|
def show_data(self, data):
|
||||||
self.animation.stop()
|
self.animation.stop()
|
||||||
if data.get('id', True) == self.data.get('id', False):
|
same_item = data.get('id', True) == self.data.get('id', False)
|
||||||
return
|
|
||||||
self.data = {'id':data.get('id', None)}
|
self.data = {'id':data.get('id', None)}
|
||||||
if data.has_key('cover'):
|
if data.has_key('cover'):
|
||||||
self.pixmap = QPixmap.fromImage(data.pop('cover'))
|
self.pixmap = QPixmap.fromImage(data.pop('cover'))
|
||||||
@ -120,7 +126,8 @@ class CoverView(QWidget): # {{{
|
|||||||
self.pixmap = self.default_pixmap
|
self.pixmap = self.default_pixmap
|
||||||
self.do_layout()
|
self.do_layout()
|
||||||
self.update()
|
self.update()
|
||||||
self.animation.start()
|
if not same_item:
|
||||||
|
self.animation.start()
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
canvas_size = self.rect()
|
canvas_size = self.rect()
|
||||||
@ -147,6 +154,7 @@ class CoverView(QWidget): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
# Book Info {{{
|
||||||
class Label(QLabel):
|
class Label(QLabel):
|
||||||
|
|
||||||
mr = pyqtSignal(object)
|
mr = pyqtSignal(object)
|
||||||
@ -174,8 +182,9 @@ class Label(QLabel):
|
|||||||
|
|
||||||
class BookInfo(QScrollArea):
|
class BookInfo(QScrollArea):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, vertical, parent=None):
|
||||||
QScrollArea.__init__(self, parent)
|
QScrollArea.__init__(self, parent)
|
||||||
|
self.vertical = vertical
|
||||||
self.setWidgetResizable(True)
|
self.setWidgetResizable(True)
|
||||||
self.label = Label()
|
self.label = Label()
|
||||||
self.setWidget(self.label)
|
self.setWidget(self.label)
|
||||||
@ -188,13 +197,25 @@ class BookInfo(QScrollArea):
|
|||||||
rows = render_rows(data)
|
rows = render_rows(data)
|
||||||
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
||||||
k, t in rows])
|
k, t in rows])
|
||||||
if _('Comments') in data and data[_('Comments')]:
|
if self.vertical:
|
||||||
comments = comments_to_html(data[_('Comments')])
|
if _('Comments') in data and data[_('Comments')]:
|
||||||
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
|
comments = comments_to_html(data[_('Comments')])
|
||||||
|
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
|
||||||
|
self.label.setText(u'<table>%s</table>'%rows)
|
||||||
|
else:
|
||||||
|
comments = ''
|
||||||
|
if _('Comments') in data:
|
||||||
|
comments = comments_to_html(data[_('Comments')])
|
||||||
|
left_pane = u'<table>%s</table>'%rows
|
||||||
|
right_pane = u'<div>%s</div>'%comments
|
||||||
|
self.label.setText(u'<table><tr><td valign="top" '
|
||||||
|
'style="padding-right:2em">%s</td><td valign="top">%s</td></tr></table>'
|
||||||
|
% (left_pane, right_pane))
|
||||||
|
|
||||||
self.label.setText(u'<table>%s</table>'%rows)
|
|
||||||
|
|
||||||
class BookDetails(QWidget):
|
# }}}
|
||||||
|
|
||||||
|
class BookDetails(QWidget): # {{{
|
||||||
|
|
||||||
resized = pyqtSignal(object)
|
resized = pyqtSignal(object)
|
||||||
show_book_info = pyqtSignal()
|
show_book_info = pyqtSignal()
|
||||||
@ -234,20 +255,26 @@ class BookDetails(QWidget):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, vertical, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
self._layout = QVBoxLayout()
|
self._layout = QVBoxLayout()
|
||||||
|
if not vertical:
|
||||||
|
self._layout.setDirection(self._layout.LeftToRight)
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
self.cover_view = CoverView(self)
|
|
||||||
|
self.cover_view = CoverView(vertical, self)
|
||||||
self.cover_view.relayout(self.size())
|
self.cover_view.relayout(self.size())
|
||||||
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
|
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
|
||||||
self._layout.addWidget(self.cover_view, alignment=Qt.AlignHCenter)
|
self._layout.addWidget(self.cover_view)
|
||||||
self.book_info = BookInfo(self)
|
self.book_info = BookInfo(vertical, self)
|
||||||
self._layout.addWidget(self.book_info)
|
self._layout.addWidget(self.book_info)
|
||||||
self.book_info.link_clicked.connect(self._link_clicked)
|
self.book_info.link_clicked.connect(self._link_clicked)
|
||||||
self.book_info.mr.connect(self.mouseReleaseEvent)
|
self.book_info.mr.connect(self.mouseReleaseEvent)
|
||||||
self.setMinimumSize(QSize(190, 200))
|
if vertical:
|
||||||
|
self.setMinimumSize(QSize(190, 200))
|
||||||
|
else:
|
||||||
|
self.setMinimumSize(120, 120)
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
def _link_clicked(self, link):
|
def _link_clicked(self, link):
|
||||||
@ -277,5 +304,5 @@ class BookDetails(QWidget):
|
|||||||
def reset_info(self):
|
def reset_info(self):
|
||||||
self.show_data({})
|
self.show_data({})
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -689,14 +689,28 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_error_dialog.show()
|
self.device_error_dialog.show()
|
||||||
|
|
||||||
# Device connected {{{
|
# Device connected {{{
|
||||||
def device_detected(self, connected, is_folder_device):
|
|
||||||
'''
|
def set_device_menu_items_state(self, connected, is_folder_device):
|
||||||
Called when a device is connected to the computer.
|
|
||||||
'''
|
|
||||||
if connected:
|
if connected:
|
||||||
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
||||||
if is_folder_device:
|
if is_folder_device:
|
||||||
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
|
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
|
||||||
|
self._sync_menu.enable_device_actions(True,
|
||||||
|
self.device_manager.device.card_prefix(),
|
||||||
|
self.device_manager.device)
|
||||||
|
self.eject_action.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
|
self._sync_menu.enable_device_actions(False)
|
||||||
|
self.eject_action.setEnabled(False)
|
||||||
|
|
||||||
|
def device_detected(self, connected, is_folder_device):
|
||||||
|
'''
|
||||||
|
Called when a device is connected to the computer.
|
||||||
|
'''
|
||||||
|
self.set_device_menu_items_state(connected, is_folder_device)
|
||||||
|
if connected:
|
||||||
self.device_manager.get_device_information(\
|
self.device_manager.get_device_information(\
|
||||||
Dispatcher(self.info_read))
|
Dispatcher(self.info_read))
|
||||||
self.set_default_thumbnail(\
|
self.set_default_thumbnail(\
|
||||||
@ -705,17 +719,10 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_manager.device.__class__.get_gui_name()+\
|
self.device_manager.device.__class__.get_gui_name()+\
|
||||||
_(' detected.'), 3000)
|
_(' detected.'), 3000)
|
||||||
self.device_connected = 'device' if not is_folder_device else 'folder'
|
self.device_connected = 'device' if not is_folder_device else 'folder'
|
||||||
self._sync_menu.enable_device_actions(True,
|
|
||||||
self.device_manager.device.card_prefix(),
|
|
||||||
self.device_manager.device)
|
|
||||||
self.location_view.model().device_connected(self.device_manager.device)
|
self.location_view.model().device_connected(self.device_manager.device)
|
||||||
self.eject_action.setEnabled(True)
|
|
||||||
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
||||||
else:
|
else:
|
||||||
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
|
||||||
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
|
||||||
self.device_connected = None
|
self.device_connected = None
|
||||||
self._sync_menu.enable_device_actions(False)
|
|
||||||
self.location_view.model().update_devices()
|
self.location_view.model().update_devices()
|
||||||
self.vanity.setText(self.vanity_template%\
|
self.vanity.setText(self.vanity_template%\
|
||||||
dict(version=self.latest_version, device=' '))
|
dict(version=self.latest_version, device=' '))
|
||||||
@ -723,7 +730,6 @@ class DeviceMixin(object): # {{{
|
|||||||
if self.current_view() != self.library_view:
|
if self.current_view() != self.library_view:
|
||||||
self.book_details.reset_info()
|
self.book_details.reset_info()
|
||||||
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||||
self.eject_action.setEnabled(False)
|
|
||||||
self.refresh_ondevice_info (device_connected = False)
|
self.refresh_ondevice_info (device_connected = False)
|
||||||
|
|
||||||
def info_read(self, job):
|
def info_read(self, job):
|
||||||
@ -1347,7 +1353,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if reset:
|
if reset:
|
||||||
# First build a cache of the library, so the search isn't On**2
|
# First build a cache of the library, so the search isn't On**2
|
||||||
self.db_book_title_cache = {}
|
self.db_book_title_cache = {}
|
||||||
self.db_book_uuid_cache = set()
|
self.db_book_uuid_cache = {}
|
||||||
db = self.library_view.model().db
|
db = self.library_view.model().db
|
||||||
for id in db.data.iterallids():
|
for id in db.data.iterallids():
|
||||||
mi = db.get_metadata(id, index_is_id=True)
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
@ -1364,7 +1370,7 @@ class DeviceMixin(object): # {{{
|
|||||||
aus = re.sub('(?u)\W|[_]', '', aus)
|
aus = re.sub('(?u)\W|[_]', '', aus)
|
||||||
self.db_book_title_cache[title]['author_sort'][aus] = mi
|
self.db_book_title_cache[title]['author_sort'][aus] = mi
|
||||||
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
|
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
|
||||||
self.db_book_uuid_cache.add(mi.uuid)
|
self.db_book_uuid_cache[mi.uuid] = mi.application_id
|
||||||
|
|
||||||
# Now iterate through all the books on the device, setting the
|
# Now iterate through all the books on the device, setting the
|
||||||
# in_library field Fastest and most accurate key is the uuid. Second is
|
# in_library field Fastest and most accurate key is the uuid. Second is
|
||||||
@ -1376,11 +1382,13 @@ class DeviceMixin(object): # {{{
|
|||||||
for book in booklist:
|
for book in booklist:
|
||||||
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
# ensure that the correct application_id is set
|
||||||
|
book.application_id = self.db_book_uuid_cache[book.uuid]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
book_title = book.title.lower() if book.title else ''
|
book_title = book.title.lower() if book.title else ''
|
||||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
||||||
book.in_library = False
|
book.in_library = None
|
||||||
d = self.db_book_title_cache.get(book_title, None)
|
d = self.db_book_title_cache.get(book_title, None)
|
||||||
if d is not None:
|
if d is not None:
|
||||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||||
|
@ -11,7 +11,8 @@ from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
|
|||||||
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||||
|
|
||||||
def __init__(self, settings, all_formats, supports_subdirs,
|
def __init__(self, settings, all_formats, supports_subdirs,
|
||||||
must_read_metadata, extra_customization_message):
|
must_read_metadata, supports_use_author_sort,
|
||||||
|
extra_customization_message):
|
||||||
|
|
||||||
QWidget.__init__(self)
|
QWidget.__init__(self)
|
||||||
Ui_ConfigWidget.__init__(self)
|
Ui_ConfigWidget.__init__(self)
|
||||||
@ -38,6 +39,10 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
||||||
else:
|
else:
|
||||||
self.opt_read_metadata.hide()
|
self.opt_read_metadata.hide()
|
||||||
|
if supports_use_author_sort:
|
||||||
|
self.opt_use_author_sort.setChecked(self.settings.use_author_sort)
|
||||||
|
else:
|
||||||
|
self.opt_use_author_sort.hide()
|
||||||
if extra_customization_message:
|
if extra_customization_message:
|
||||||
self.extra_customization_label.setText(extra_customization_message)
|
self.extra_customization_label.setText(extra_customization_message)
|
||||||
if settings.extra_customization:
|
if settings.extra_customization:
|
||||||
@ -69,3 +74,6 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
|
|
||||||
def read_metadata(self):
|
def read_metadata(self):
|
||||||
return self.opt_read_metadata.isChecked()
|
return self.opt_read_metadata.isChecked()
|
||||||
|
|
||||||
|
def use_author_sort(self):
|
||||||
|
return self.opt_use_author_sort.isChecked()
|
||||||
|
@ -90,7 +90,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="3" column="0">
|
||||||
|
<widget class="QCheckBox" name="opt_use_author_sort">
|
||||||
|
<property name="text">
|
||||||
|
<string>Use author sort for author</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="extra_customization_label">
|
<widget class="QLabel" name="extra_customization_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Extra customization</string>
|
<string>Extra customization</string>
|
||||||
@ -103,10 +110,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLineEdit" name="opt_extra_customization"/>
|
<widget class="QLineEdit" name="opt_extra_customization"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Save &template:</string>
|
<string>Save &template:</string>
|
||||||
@ -116,7 +123,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLineEdit" name="opt_save_template"/>
|
<widget class="QLineEdit" name="opt_save_template"/>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>884</width>
|
<width>1000</width>
|
||||||
<height>730</height>
|
<height>730</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>604</width>
|
<width>720</width>
|
||||||
<height>679</height>
|
<height>679</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -370,7 +370,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="5" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="show_avg_rating">
|
<widget class="QCheckBox" name="show_avg_rating">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &average ratings in the tags browser</string>
|
<string>Show &average ratings in the tags browser</string>
|
||||||
|
@ -49,6 +49,9 @@ class SocialMetadata(QDialog):
|
|||||||
self.mi.tags = self.worker.mi.tags
|
self.mi.tags = self.worker.mi.tags
|
||||||
self.mi.rating = self.worker.mi.rating
|
self.mi.rating = self.worker.mi.rating
|
||||||
self.mi.comments = self.worker.mi.comments
|
self.mi.comments = self.worker.mi.comments
|
||||||
|
if self.worker.mi.series:
|
||||||
|
self.mi.series = self.worker.mi.series
|
||||||
|
self.mi.series_index = self.worker.mi.series_index
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
109
src/calibre/gui2/dialogs/delete_matching_from_device.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
|
||||||
|
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string, \
|
||||||
|
title_sort
|
||||||
|
from calibre.gui2.dialogs.delete_matching_from_device_ui import \
|
||||||
|
Ui_DeleteMatchingFromDeviceDialog
|
||||||
|
from calibre.utils.date import UNDEFINED_DATE
|
||||||
|
|
||||||
|
class tableItem(QTableWidgetItem):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
QTableWidgetItem.__init__(self, text)
|
||||||
|
self.setFlags(Qt.ItemIsEnabled)
|
||||||
|
self.sort = text.lower()
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.sort >= other.sort
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.sort < other.sort
|
||||||
|
|
||||||
|
class titleTableItem(tableItem):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
tableItem.__init__(self, text)
|
||||||
|
self.sort = title_sort(text.lower())
|
||||||
|
|
||||||
|
class authorTableItem(tableItem):
|
||||||
|
|
||||||
|
def __init__(self, book):
|
||||||
|
tableItem.__init__(self, authors_to_string(book.authors))
|
||||||
|
if book.author_sort is not None:
|
||||||
|
self.sort = book.author_sort.lower()
|
||||||
|
else:
|
||||||
|
self.sort = authors_to_sort_string(book.authors).lower()
|
||||||
|
|
||||||
|
class dateTableItem(tableItem):
|
||||||
|
|
||||||
|
def __init__(self, date):
|
||||||
|
if date is not None:
|
||||||
|
tableItem.__init__(self, strftime('%x', date))
|
||||||
|
self.sort = date
|
||||||
|
else:
|
||||||
|
tableItem.__init__(self, '')
|
||||||
|
self.sort = UNDEFINED_DATE
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, items):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
Ui_DeleteMatchingFromDeviceDialog.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.explanation.setText('<p>'+_('All checked books will be '
|
||||||
|
'<b>permanently deleted</b> from your '
|
||||||
|
'device. Please verify the list.'+'</p>'))
|
||||||
|
self.buttonBox.accepted.connect(self.accepted)
|
||||||
|
self.table.cellClicked.connect(self.cell_clicked)
|
||||||
|
self.table.setSelectionMode(QAbstractItemView.NoSelection)
|
||||||
|
self.table.setColumnCount(5)
|
||||||
|
self.table.setHorizontalHeaderLabels(
|
||||||
|
['', _('Location'), _('Title'),
|
||||||
|
_('Author'), _('Date'), _('Format')])
|
||||||
|
rows = 0
|
||||||
|
for card in items:
|
||||||
|
rows += len(items[card][1])
|
||||||
|
self.table.setRowCount(rows)
|
||||||
|
row = 0
|
||||||
|
for card in items:
|
||||||
|
(model,books) = items[card]
|
||||||
|
for (id,book) in books:
|
||||||
|
item = QTableWidgetItem()
|
||||||
|
item.setFlags(Qt.ItemIsUserCheckable|Qt.ItemIsEnabled)
|
||||||
|
item.setCheckState(Qt.Checked)
|
||||||
|
item.setData(Qt.UserRole, (model, id, book.path))
|
||||||
|
self.table.setItem(row, 0, item)
|
||||||
|
self.table.setItem(row, 1, tableItem(card))
|
||||||
|
self.table.setItem(row, 2, titleTableItem(book.title))
|
||||||
|
self.table.setItem(row, 3, authorTableItem(book))
|
||||||
|
self.table.setItem(row, 4, dateTableItem(book.datetime))
|
||||||
|
self.table.setItem(row, 5, tableItem(book.path.rpartition('.')[2]))
|
||||||
|
row += 1
|
||||||
|
self.table.setCurrentCell(0, 1)
|
||||||
|
self.table.resizeColumnsToContents()
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
|
self.table.sortByColumn(2, Qt.AscendingOrder)
|
||||||
|
self.table.setCurrentCell(0, 1)
|
||||||
|
|
||||||
|
def cell_clicked(self, row, col):
|
||||||
|
if col == 0:
|
||||||
|
self.table.setCurrentCell(row, 1)
|
||||||
|
|
||||||
|
def accepted(self):
|
||||||
|
self.result = []
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
if self.table.item(row, 0).checkState() == Qt.Unchecked:
|
||||||
|
continue
|
||||||
|
(model, id, path) = self.table.item(row, 0).data(Qt.UserRole).toPyObject()
|
||||||
|
path = unicode(path)
|
||||||
|
self.result.append((model, id, path))
|
||||||
|
return
|
||||||
|
|
90
src/calibre/gui2/dialogs/delete_matching_from_device.ui
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>DeleteMatchingFromDeviceDialog</class>
|
||||||
|
<widget class="QDialog" name="DeleteMatchingFromDeviceDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>730</width>
|
||||||
|
<height>342</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Delete from device</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="explanation">
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTableWidget" name="table">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="columnCount">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
<property name="centerButtons">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>DeleteMatchingFromDeviceDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>229</x>
|
||||||
|
<y>211</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>234</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>DeleteMatchingFromDeviceDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>297</x>
|
||||||
|
<y>217</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>234</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -8,17 +8,18 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
||||||
QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy
|
QSize, QSizePolicy, QStatusBar
|
||||||
|
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import isosx, __appname__
|
from calibre.constants import isosx, __appname__, preferred_encoding
|
||||||
from calibre.gui2 import config, is_widescreen
|
from calibre.gui2 import config, is_widescreen
|
||||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||||
from calibre.gui2.widgets import Splitter
|
from calibre.gui2.widgets import Splitter
|
||||||
from calibre.gui2.tag_view import TagBrowserWidget
|
from calibre.gui2.tag_view import TagBrowserWidget
|
||||||
from calibre.gui2.status import StatusBar, HStatusBar
|
|
||||||
from calibre.gui2.book_details import BookDetails
|
from calibre.gui2.book_details import BookDetails
|
||||||
|
from calibre.gui2.notify import get_notifier
|
||||||
|
|
||||||
|
|
||||||
_keep_refs = []
|
_keep_refs = []
|
||||||
|
|
||||||
@ -130,6 +131,10 @@ class ToolbarMixin(object): # {{{
|
|||||||
self.delete_all_but_selected_formats)
|
self.delete_all_but_selected_formats)
|
||||||
self.delete_menu.addAction(
|
self.delete_menu.addAction(
|
||||||
_('Remove covers from selected books'), self.delete_covers)
|
_('Remove covers from selected books'), self.delete_covers)
|
||||||
|
self.delete_menu.addSeparator()
|
||||||
|
self.delete_menu.addAction(
|
||||||
|
_('Remove matching books from device'),
|
||||||
|
self.remove_matching_books_from_device)
|
||||||
self.action_del.setMenu(self.delete_menu)
|
self.action_del.setMenu(self.delete_menu)
|
||||||
|
|
||||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
||||||
@ -158,8 +163,7 @@ class ToolbarMixin(object): # {{{
|
|||||||
self.convert_menu = cm
|
self.convert_menu = cm
|
||||||
|
|
||||||
pm = QMenu()
|
pm = QMenu()
|
||||||
ap = self.action_preferences
|
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
||||||
pm.addAction(ap)
|
|
||||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
||||||
self.run_wizard)
|
self.run_wizard)
|
||||||
self.action_preferences.setMenu(pm)
|
self.action_preferences.setMenu(pm)
|
||||||
@ -332,26 +336,24 @@ class Stack(QStackedWidget): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class SideBar(QToolBar): # {{{
|
class StatusBar(QStatusBar): # {{{
|
||||||
|
|
||||||
|
def initialize(self, systray=None):
|
||||||
|
self.systray = systray
|
||||||
|
self.notifier = get_notifier(systray)
|
||||||
|
|
||||||
def __init__(self, splitters, jobs_button, parent=None):
|
def show_message(self, msg, timeout=0):
|
||||||
QToolBar.__init__(self, _('Side bar'), parent)
|
QStatusBar.showMessage(self, msg, timeout)
|
||||||
self.setOrientation(Qt.Vertical)
|
if self.notifier is not None and not config['disable_tray_notification']:
|
||||||
self.setMovable(False)
|
if isosx and isinstance(msg, unicode):
|
||||||
self.setFloatable(False)
|
try:
|
||||||
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
msg = msg.encode(preferred_encoding)
|
||||||
self.setIconSize(QSize(48, 48))
|
except UnicodeEncodeError:
|
||||||
self.spacer = QWidget(self)
|
msg = msg.encode('utf-8')
|
||||||
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
self.notifier(msg)
|
||||||
for s in splitters:
|
|
||||||
self.addWidget(s.button)
|
|
||||||
self.addWidget(self.spacer)
|
|
||||||
self.addWidget(jobs_button)
|
|
||||||
|
|
||||||
for ch in self.children():
|
def clear_message(self):
|
||||||
if isinstance(ch, QToolButton):
|
QStatusBar.clearMessage(self)
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -361,50 +363,52 @@ class LayoutMixin(object): # {{{
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle(__appname__)
|
self.setWindowTitle(__appname__)
|
||||||
|
|
||||||
if config['gui_layout'] == 'narrow':
|
if config['gui_layout'] == 'narrow': # narrow {{{
|
||||||
self.status_bar = self.book_details = StatusBar(self)
|
self.book_details = BookDetails(False, self)
|
||||||
self.stack = Stack(self)
|
self.stack = Stack(self)
|
||||||
self.bd_splitter = Splitter('book_details_splitter',
|
self.bd_splitter = Splitter('book_details_splitter',
|
||||||
_('Book Details'), I('book.svg'),
|
_('Book Details'), I('book.svg'),
|
||||||
orientation=Qt.Vertical, parent=self, side_index=1)
|
orientation=Qt.Vertical, parent=self, side_index=1)
|
||||||
self._layout_mem = [QWidget(self), QHBoxLayout()]
|
self.bd_splitter.addWidget(self.stack)
|
||||||
self._layout_mem[0].setLayout(self._layout_mem[1])
|
self.bd_splitter.addWidget(self.book_details)
|
||||||
l = self._layout_mem[1]
|
|
||||||
l.addWidget(self.stack)
|
|
||||||
self.sidebar = SideBar([getattr(self, x+'_splitter')
|
|
||||||
for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self)
|
|
||||||
l.addWidget(self.sidebar)
|
|
||||||
self.bd_splitter.addWidget(self._layout_mem[0])
|
|
||||||
self.bd_splitter.addWidget(self.status_bar)
|
|
||||||
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
||||||
self.centralwidget.layout().addWidget(self.bd_splitter)
|
self.centralwidget.layout().addWidget(self.bd_splitter)
|
||||||
else:
|
# }}}
|
||||||
self.status_bar = HStatusBar(self)
|
else: # wide {{{
|
||||||
self.setStatusBar(self.status_bar)
|
|
||||||
self.bd_splitter = Splitter('book_details_splitter',
|
self.bd_splitter = Splitter('book_details_splitter',
|
||||||
_('Book Details'), I('book.svg'), initial_side_size=200,
|
_('Book Details'), I('book.svg'), initial_side_size=200,
|
||||||
orientation=Qt.Horizontal, parent=self, side_index=1)
|
orientation=Qt.Horizontal, parent=self, side_index=1)
|
||||||
self.stack = Stack(self)
|
self.stack = Stack(self)
|
||||||
self.bd_splitter.addWidget(self.stack)
|
self.bd_splitter.addWidget(self.stack)
|
||||||
self.book_details = BookDetails(self)
|
self.book_details = BookDetails(True, self)
|
||||||
self.bd_splitter.addWidget(self.book_details)
|
self.bd_splitter.addWidget(self.book_details)
|
||||||
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
||||||
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
||||||
QSizePolicy.Expanding))
|
QSizePolicy.Expanding))
|
||||||
self.centralwidget.layout().addWidget(self.bd_splitter)
|
self.centralwidget.layout().addWidget(self.bd_splitter)
|
||||||
|
# }}}
|
||||||
|
|
||||||
for x in ('cb', 'tb', 'bd'):
|
self.status_bar = StatusBar(self)
|
||||||
button = getattr(self, x+'_splitter').button
|
for x in ('cb', 'tb', 'bd'):
|
||||||
button.setIconSize(QSize(22, 22))
|
button = getattr(self, x+'_splitter').button
|
||||||
self.status_bar.addPermanentWidget(button)
|
button.setIconSize(QSize(24, 24))
|
||||||
self.status_bar.addPermanentWidget(self.jobs_button)
|
self.status_bar.addPermanentWidget(button)
|
||||||
|
self.status_bar.addPermanentWidget(self.jobs_button)
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
|
||||||
def finalize_layout(self):
|
def finalize_layout(self):
|
||||||
|
self.status_bar.initialize(self.system_tray_icon)
|
||||||
|
self.book_details.show_book_info.connect(self.show_book_info)
|
||||||
|
self.book_details.files_dropped.connect(self.files_dropped_on_book)
|
||||||
|
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
|
||||||
|
self.book_details.view_specific_format.connect(self.view_format_by_id)
|
||||||
|
|
||||||
m = self.library_view.model()
|
m = self.library_view.model()
|
||||||
if m.rowCount(None) > 0:
|
if m.rowCount(None) > 0:
|
||||||
self.library_view.set_current_row(0)
|
self.library_view.set_current_row(0)
|
||||||
m.current_changed(self.library_view.currentIndex(),
|
m.current_changed(self.library_view.currentIndex(),
|
||||||
self.library_view.currentIndex())
|
self.library_view.currentIndex())
|
||||||
|
self.library_view.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
|
||||||
def save_layout_state(self):
|
def save_layout_state(self):
|
||||||
|
@ -769,6 +769,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{
|
|||||||
'format',
|
'format',
|
||||||
'formats',
|
'formats',
|
||||||
'title',
|
'title',
|
||||||
|
'inlibrary'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -807,12 +808,23 @@ class OnDeviceSearch(SearchQueryParser): # {{{
|
|||||||
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
|
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
|
||||||
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
|
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
|
||||||
'format':lambda x: os.path.splitext(x.path)[1].lower(),
|
'format':lambda x: os.path.splitext(x.path)[1].lower(),
|
||||||
|
'inlibrary':lambda x : getattr(x, 'in_library')
|
||||||
}
|
}
|
||||||
for x in ('author', 'format'):
|
for x in ('author', 'format'):
|
||||||
q[x+'s'] = q[x]
|
q[x+'s'] = q[x]
|
||||||
for index, row in enumerate(self.model.db):
|
for index, row in enumerate(self.model.db):
|
||||||
for locvalue in locations:
|
for locvalue in locations:
|
||||||
accessor = q[locvalue]
|
accessor = q[locvalue]
|
||||||
|
if query == 'true':
|
||||||
|
if accessor(row) is not None:
|
||||||
|
matches.add(index)
|
||||||
|
continue
|
||||||
|
if query == 'false':
|
||||||
|
if accessor(row) is None:
|
||||||
|
matches.add(index)
|
||||||
|
continue
|
||||||
|
if locvalue == 'inlibrary':
|
||||||
|
continue # this is bool, so can't match below
|
||||||
try:
|
try:
|
||||||
### Can't separate authors because comma is used for name sep and author sep
|
### Can't separate authors because comma is used for name sep and author sep
|
||||||
### Exact match might not get what you want. For that reason, turn author
|
### Exact match might not get what you want. For that reason, turn author
|
||||||
@ -862,11 +874,15 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
self.editable = True
|
self.editable = True
|
||||||
self.book_in_library = None
|
self.book_in_library = None
|
||||||
|
|
||||||
def mark_for_deletion(self, job, rows):
|
def mark_for_deletion(self, job, rows, rows_are_ids=False):
|
||||||
self.marked_for_deletion[job] = self.indices(rows)
|
if rows_are_ids:
|
||||||
for row in rows:
|
self.marked_for_deletion[job] = rows
|
||||||
indices = self.row_indices(row)
|
self.reset()
|
||||||
self.dataChanged.emit(indices[0], indices[-1])
|
else:
|
||||||
|
self.marked_for_deletion[job] = self.indices(rows)
|
||||||
|
for row in rows:
|
||||||
|
indices = self.row_indices(row)
|
||||||
|
self.dataChanged.emit(indices[0], indices[-1])
|
||||||
|
|
||||||
def deletion_done(self, job, succeeded=True):
|
def deletion_done(self, job, succeeded=True):
|
||||||
if not self.marked_for_deletion.has_key(job):
|
if not self.marked_for_deletion.has_key(job):
|
||||||
@ -888,13 +904,13 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
ans.extend(v)
|
ans.extend(v)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def clear_ondevice(self, db_ids):
|
def clear_ondevice(self, db_ids, to_what=None):
|
||||||
for data in self.db:
|
for data in self.db:
|
||||||
if data is None:
|
if data is None:
|
||||||
continue
|
continue
|
||||||
app_id = getattr(data, 'application_id', None)
|
app_id = getattr(data, 'application_id', None)
|
||||||
if app_id is not None and app_id in db_ids:
|
if app_id is not None and app_id in db_ids:
|
||||||
data.in_library = False
|
data.in_library = to_what
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
@ -1049,6 +1065,13 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
def paths(self, rows):
|
def paths(self, rows):
|
||||||
return [self.db[self.map[r.row()]].path for r in rows ]
|
return [self.db[self.map[r.row()]].path for r in rows ]
|
||||||
|
|
||||||
|
def paths_for_db_ids(self, db_ids):
|
||||||
|
res = []
|
||||||
|
for r,b in enumerate(self.db):
|
||||||
|
if b.application_id in db_ids:
|
||||||
|
res.append((r,b))
|
||||||
|
return res
|
||||||
|
|
||||||
def indices(self, rows):
|
def indices(self, rows):
|
||||||
'''
|
'''
|
||||||
Return indices into underlying database from rows
|
Return indices into underlying database from rows
|
||||||
@ -1089,6 +1112,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
||||||
if self.db[self.map[row]].in_library:
|
if self.db[self.map[row]].in_library:
|
||||||
return QVariant(self.bool_yes_icon)
|
return QVariant(self.bool_yes_icon)
|
||||||
|
elif self.db[self.map[row]].in_library is not None:
|
||||||
|
return QVariant(self.bool_no_icon)
|
||||||
elif role == Qt.TextAlignmentRole:
|
elif role == Qt.TextAlignmentRole:
|
||||||
cname = self.column_map[index.column()]
|
cname = self.column_map[index.column()]
|
||||||
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
||||||
|
@ -84,12 +84,12 @@ class DownloadMetadata(Thread):
|
|||||||
if mi.isbn:
|
if mi.isbn:
|
||||||
args['isbn'] = mi.isbn
|
args['isbn'] = mi.isbn
|
||||||
else:
|
else:
|
||||||
if not mi.title:
|
if not mi.title or mi.title == _('Unknown'):
|
||||||
self.failures[id] = \
|
self.failures[id] = \
|
||||||
(str(id), _('Book has neither title nor ISBN'))
|
(str(id), _('Book has neither title nor ISBN'))
|
||||||
continue
|
continue
|
||||||
args['title'] = mi.title
|
args['title'] = mi.title
|
||||||
if mi.authors:
|
if mi.authors and mi.authors[0] != _('Unknown'):
|
||||||
args['author'] = mi.authors[0]
|
args['author'] = mi.authors[0]
|
||||||
if self.key:
|
if self.key:
|
||||||
args['isbndb_key'] = self.key
|
args['isbndb_key'] = self.key
|
||||||
@ -127,6 +127,10 @@ class DownloadMetadata(Thread):
|
|||||||
self.db.set_tags(id, mi.tags)
|
self.db.set_tags(id, mi.tags)
|
||||||
if mi.comments:
|
if mi.comments:
|
||||||
self.db.set_comment(id, mi.comments)
|
self.db.set_comment(id, mi.comments)
|
||||||
|
if mi.series:
|
||||||
|
self.db.set_series(id, mi.series)
|
||||||
|
if mi.series_index is not None:
|
||||||
|
self.db.set_series_index(id, mi.series_index)
|
||||||
|
|
||||||
self.updated = set(self.fetched_metadata)
|
self.updated = set(self.fetched_metadata)
|
||||||
|
|
||||||
|
@ -18,17 +18,20 @@ from calibre.utils.config import prefs
|
|||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
|
||||||
class SearchLineEdit(QLineEdit):
|
class SearchLineEdit(QLineEdit):
|
||||||
|
key_pressed = pyqtSignal(object)
|
||||||
|
mouse_released = pyqtSignal(object)
|
||||||
|
focus_out = pyqtSignal(object)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
self.emit(SIGNAL('key_pressed(PyQt_PyObject)'), event)
|
self.key_pressed.emit(event)
|
||||||
QLineEdit.keyPressEvent(self, event)
|
QLineEdit.keyPressEvent(self, event)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
|
self.mouse_released.emit(event)
|
||||||
QLineEdit.mouseReleaseEvent(self, event)
|
QLineEdit.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
def focusOutEvent(self, event):
|
def focusOutEvent(self, event):
|
||||||
self.emit(SIGNAL('focus_out(PyQt_PyObject)'), event)
|
self.focus_out.emit(event)
|
||||||
QLineEdit.focusOutEvent(self, event)
|
QLineEdit.focusOutEvent(self, event)
|
||||||
|
|
||||||
def dropEvent(self, ev):
|
def dropEvent(self, ev):
|
||||||
@ -68,10 +71,10 @@ class SearchBox2(QComboBox):
|
|||||||
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
||||||
self.line_edit = SearchLineEdit(self)
|
self.line_edit = SearchLineEdit(self)
|
||||||
self.setLineEdit(self.line_edit)
|
self.setLineEdit(self.line_edit)
|
||||||
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
|
self.line_edit.key_pressed.connect(self.key_pressed,
|
||||||
self.key_pressed, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
self.line_edit.mouse_released.connect(self.mouse_released,
|
||||||
self.mouse_released, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
self.help_state = False
|
self.help_state = False
|
||||||
self.as_you_type = True
|
self.as_you_type = True
|
||||||
@ -90,14 +93,18 @@ class SearchBox2(QComboBox):
|
|||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
self.colorize = colorize
|
self.colorize = colorize
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
self.connect(self, SIGNAL('editTextChanged(QString)'), self.text_edited_slot)
|
|
||||||
|
|
||||||
def normalize_state(self):
|
def normalize_state(self):
|
||||||
self.setEditText('')
|
if self.help_state:
|
||||||
self.line_edit.setStyleSheet(
|
self.setEditText('')
|
||||||
'QLineEdit { color: black; background-color: %s; }' %
|
self.line_edit.setStyleSheet(
|
||||||
self.normal_background)
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
self.help_state = False
|
self.normal_background)
|
||||||
|
self.help_state = False
|
||||||
|
else:
|
||||||
|
self.line_edit.setStyleSheet(
|
||||||
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
|
self.normal_background)
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
if self.help_state:
|
if self.help_state:
|
||||||
@ -131,17 +138,13 @@ class SearchBox2(QComboBox):
|
|||||||
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
|
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
|
||||||
|
|
||||||
def key_pressed(self, event):
|
def key_pressed(self, event):
|
||||||
if self.help_state:
|
self.normalize_state()
|
||||||
self.normalize_state()
|
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||||
if not self.as_you_type:
|
self.do_search()
|
||||||
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||||
self.do_search()
|
|
||||||
|
|
||||||
def mouse_released(self, event):
|
def mouse_released(self, event):
|
||||||
if self.help_state:
|
self.normalize_state()
|
||||||
self.normalize_state()
|
|
||||||
|
|
||||||
def text_edited_slot(self, text):
|
|
||||||
if self.as_you_type:
|
if self.as_you_type:
|
||||||
self.timer = self.startTimer(self.__class__.INTERVAL)
|
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||||
|
|
||||||
@ -227,14 +230,13 @@ class SavedSearchBox(QComboBox):
|
|||||||
|
|
||||||
self.line_edit = SearchLineEdit(self)
|
self.line_edit = SearchLineEdit(self)
|
||||||
self.setLineEdit(self.line_edit)
|
self.setLineEdit(self.line_edit)
|
||||||
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
|
self.line_edit.key_pressed.connect(self.key_pressed,
|
||||||
self.key_pressed, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
self.line_edit.mouse_released.connect(self.mouse_released,
|
||||||
self.mouse_released, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('focus_out(PyQt_PyObject)'),
|
self.line_edit.focus_out.connect(self.focus_out,
|
||||||
self.focus_out, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self, SIGNAL('activated(const QString&)'),
|
self.activated[str].connect(self.saved_search_selected)
|
||||||
self.saved_search_selected)
|
|
||||||
|
|
||||||
completer = QCompleter(self) # turn off auto-completion
|
completer = QCompleter(self) # turn off auto-completion
|
||||||
self.setCompleter(completer)
|
self.setCompleter(completer)
|
||||||
@ -282,7 +284,7 @@ class SavedSearchBox(QComboBox):
|
|||||||
if self.help_state:
|
if self.help_state:
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
|
|
||||||
def saved_search_selected (self, qname):
|
def saved_search_selected(self, qname):
|
||||||
qname = unicode(qname)
|
qname = unicode(qname)
|
||||||
if qname is None or not qname.strip():
|
if qname is None or not qname.strip():
|
||||||
return
|
return
|
||||||
|
@ -1,253 +0,0 @@
|
|||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
|
|
||||||
QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
|
|
||||||
QPropertyAnimation, QEasingCurve, QDesktopServices, QUrl
|
|
||||||
|
|
||||||
|
|
||||||
from calibre import fit_image, preferred_encoding, isosx
|
|
||||||
from calibre.gui2 import config
|
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
|
||||||
from calibre.gui2.notify import get_notifier
|
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
|
||||||
from calibre.library.comments import comments_to_html
|
|
||||||
from calibre.gui2.book_details import render_rows
|
|
||||||
|
|
||||||
class BookInfoDisplay(QWidget):
|
|
||||||
|
|
||||||
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
|
|
||||||
files_dropped = pyqtSignal(object, object)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def paths_from_event(cls, event):
|
|
||||||
'''
|
|
||||||
Accept a drop event and return a list of paths that can be read from
|
|
||||||
and represent files with extensions.
|
|
||||||
'''
|
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
|
||||||
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
|
||||||
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
|
||||||
return
|
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
if paths:
|
|
||||||
event.acceptProposedAction()
|
|
||||||
|
|
||||||
def dropEvent(self, event):
|
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
|
||||||
self.files_dropped.emit(event, paths)
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
|
||||||
event.acceptProposedAction()
|
|
||||||
|
|
||||||
|
|
||||||
class BookCoverDisplay(QLabel): # {{{
|
|
||||||
|
|
||||||
def __init__(self, coverpath=I('book.svg')):
|
|
||||||
QLabel.__init__(self)
|
|
||||||
self.animation = QPropertyAnimation(self, 'size', self)
|
|
||||||
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
|
|
||||||
self.animation.setDuration(1000)
|
|
||||||
self.animation.setStartValue(QSize(0, 0))
|
|
||||||
self.setMaximumWidth(81)
|
|
||||||
self.setMaximumHeight(108)
|
|
||||||
self.default_pixmap = QPixmap(coverpath)
|
|
||||||
self.setScaledContents(True)
|
|
||||||
self.statusbar_height = 120
|
|
||||||
self.setPixmap(self.default_pixmap)
|
|
||||||
|
|
||||||
def do_layout(self):
|
|
||||||
self.animation.stop()
|
|
||||||
pixmap = self.pixmap()
|
|
||||||
pwidth, pheight = pixmap.width(), pixmap.height()
|
|
||||||
width, height = fit_image(pwidth, pheight,
|
|
||||||
pwidth, self.statusbar_height-20)[1:]
|
|
||||||
self.setMaximumHeight(height)
|
|
||||||
try:
|
|
||||||
aspect_ratio = pwidth/float(pheight)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
aspect_ratio = 1
|
|
||||||
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
|
|
||||||
self.animation.setEndValue(self.maximumSize())
|
|
||||||
|
|
||||||
def setPixmap(self, pixmap):
|
|
||||||
QLabel.setPixmap(self, pixmap)
|
|
||||||
self.do_layout()
|
|
||||||
self.animation.start()
|
|
||||||
|
|
||||||
def sizeHint(self):
|
|
||||||
return QSize(self.maximumWidth(), self.maximumHeight())
|
|
||||||
|
|
||||||
def relayout(self, statusbar_size):
|
|
||||||
self.statusbar_height = statusbar_size.height()
|
|
||||||
self.do_layout()
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class BookDataDisplay(QLabel):
|
|
||||||
|
|
||||||
mr = pyqtSignal(object)
|
|
||||||
link_clicked = pyqtSignal(object)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
QLabel.__init__(self)
|
|
||||||
self.setText('')
|
|
||||||
self.setWordWrap(True)
|
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
|
||||||
self.linkActivated.connect(self.link_activated)
|
|
||||||
self._link_clicked = False
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
|
||||||
QLabel.mouseReleaseEvent(self, ev)
|
|
||||||
if not self._link_clicked:
|
|
||||||
self.mr.emit(ev)
|
|
||||||
self._link_clicked = False
|
|
||||||
|
|
||||||
def link_activated(self, link):
|
|
||||||
self._link_clicked = True
|
|
||||||
link = unicode(link)
|
|
||||||
self.link_clicked.emit(link)
|
|
||||||
|
|
||||||
show_book_info = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, clear_message):
|
|
||||||
QWidget.__init__(self)
|
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
|
||||||
self._layout = QHBoxLayout()
|
|
||||||
self.setLayout(self._layout)
|
|
||||||
self.clear_message = clear_message
|
|
||||||
self.cover_display = BookInfoDisplay.BookCoverDisplay()
|
|
||||||
self._layout.addWidget(self.cover_display)
|
|
||||||
self.book_data = BookInfoDisplay.BookDataDisplay()
|
|
||||||
self.book_data.mr.connect(self.mouseReleaseEvent)
|
|
||||||
self._layout.addWidget(self.book_data)
|
|
||||||
self.data = {}
|
|
||||||
self.setVisible(False)
|
|
||||||
self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
|
||||||
ev.accept()
|
|
||||||
self.show_book_info.emit()
|
|
||||||
|
|
||||||
def show_data(self, data):
|
|
||||||
if data.has_key('cover'):
|
|
||||||
self.cover_display.setPixmap(QPixmap.fromImage(data.pop('cover')))
|
|
||||||
else:
|
|
||||||
self.cover_display.setPixmap(self.cover_display.default_pixmap)
|
|
||||||
|
|
||||||
rows, comments = [], ''
|
|
||||||
self.book_data.setText('')
|
|
||||||
self.data = data.copy()
|
|
||||||
rows = render_rows(self.data)
|
|
||||||
rows = '\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
|
||||||
k, t in rows])
|
|
||||||
if _('Comments') in self.data:
|
|
||||||
comments = comments_to_html(self.data[_('Comments')])
|
|
||||||
comments = ('<b>%s:</b>'%_('Comments'))+comments
|
|
||||||
left_pane = u'<table>%s</table>'%rows
|
|
||||||
right_pane = u'<div>%s</div>'%comments
|
|
||||||
self.book_data.setText(u'<table><tr><td valign="top" '
|
|
||||||
'style="padding-right:2em">%s</td><td valign="top">%s</td></tr></table>'
|
|
||||||
% (left_pane, right_pane))
|
|
||||||
|
|
||||||
self.clear_message()
|
|
||||||
self.book_data.updateGeometry()
|
|
||||||
self.updateGeometry()
|
|
||||||
self.setVisible(True)
|
|
||||||
self.setToolTip('<p>'+_('Click to open Book Details window') +
|
|
||||||
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class StatusBarInterface(object):
|
|
||||||
|
|
||||||
def initialize(self, systray=None):
|
|
||||||
self.systray = systray
|
|
||||||
self.notifier = get_notifier(systray)
|
|
||||||
|
|
||||||
def show_message(self, msg, timeout=0):
|
|
||||||
QStatusBar.showMessage(self, msg, timeout)
|
|
||||||
if self.notifier is not None and not config['disable_tray_notification']:
|
|
||||||
if isosx and isinstance(msg, unicode):
|
|
||||||
try:
|
|
||||||
msg = msg.encode(preferred_encoding)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
msg = msg.encode('utf-8')
|
|
||||||
self.notifier(msg)
|
|
||||||
|
|
||||||
def clear_message(self):
|
|
||||||
QStatusBar.clearMessage(self)
|
|
||||||
|
|
||||||
class BookDetailsInterface(object):
|
|
||||||
|
|
||||||
# These signals must be defined in the class implementing this interface
|
|
||||||
files_dropped = None
|
|
||||||
show_book_info = None
|
|
||||||
open_containing_folder = None
|
|
||||||
view_specific_format = None
|
|
||||||
|
|
||||||
def reset_info(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def show_data(self, data):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
class HStatusBar(QStatusBar, StatusBarInterface):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
|
|
||||||
|
|
||||||
files_dropped = pyqtSignal(object, object)
|
|
||||||
show_book_info = pyqtSignal()
|
|
||||||
open_containing_folder = pyqtSignal(int)
|
|
||||||
view_specific_format = pyqtSignal(int, object)
|
|
||||||
|
|
||||||
resized = pyqtSignal(object)
|
|
||||||
|
|
||||||
def initialize(self, systray=None):
|
|
||||||
StatusBarInterface.initialize(self, systray=systray)
|
|
||||||
self.book_info = BookInfoDisplay(self.clear_message)
|
|
||||||
self.book_info.setAcceptDrops(True)
|
|
||||||
self.scroll_area = QScrollArea()
|
|
||||||
self.scroll_area.setWidget(self.book_info)
|
|
||||||
self.scroll_area.setWidgetResizable(True)
|
|
||||||
self.book_info.show_book_info.connect(self.show_book_info.emit,
|
|
||||||
type=Qt.QueuedConnection)
|
|
||||||
self.book_info.files_dropped.connect(self.files_dropped.emit,
|
|
||||||
type=Qt.QueuedConnection)
|
|
||||||
self.book_info.book_data.link_clicked.connect(self._link_clicked)
|
|
||||||
self.addWidget(self.scroll_area, 100)
|
|
||||||
self.setMinimumHeight(120)
|
|
||||||
self.resized.connect(self.book_info.cover_display.relayout)
|
|
||||||
self.book_info.cover_display.relayout(self.size())
|
|
||||||
|
|
||||||
|
|
||||||
def _link_clicked(self, link):
|
|
||||||
typ, _, val = link.partition(':')
|
|
||||||
if typ == 'path':
|
|
||||||
self.open_containing_folder.emit(int(val))
|
|
||||||
elif typ == 'format':
|
|
||||||
id_, fmt = val.split(':')
|
|
||||||
self.view_specific_format.emit(int(id_), fmt)
|
|
||||||
elif typ == 'devpath':
|
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
|
|
||||||
|
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
|
||||||
self.resized.emit(self.size())
|
|
||||||
|
|
||||||
def reset_info(self):
|
|
||||||
self.book_info.show_data({})
|
|
||||||
|
|
||||||
def show_data(self, data):
|
|
||||||
self.book_info.show_data(data)
|
|
||||||
|
|
@ -126,8 +126,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
# Jobs Button {{{
|
# Jobs Button {{{
|
||||||
self.job_manager = JobManager()
|
self.job_manager = JobManager()
|
||||||
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
self.jobs_dialog = JobsDialog(self, self.job_manager)
|
||||||
self.jobs_button = JobsButton(horizontal=config['gui_layout'] !=
|
self.jobs_button = JobsButton(horizontal=True)
|
||||||
'narrow')
|
|
||||||
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -216,12 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
|
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
|
||||||
self.device_info = ' '
|
self.device_info = ' '
|
||||||
UpdateMixin.__init__(self, opts)
|
UpdateMixin.__init__(self, opts)
|
||||||
####################### Status Bar #####################
|
|
||||||
self.status_bar.initialize(self.system_tray_icon)
|
|
||||||
self.book_details.show_book_info.connect(self.show_book_info)
|
|
||||||
self.book_details.files_dropped.connect(self.files_dropped_on_book)
|
|
||||||
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
|
|
||||||
self.book_details.view_specific_format.connect(self.view_format_by_id)
|
|
||||||
|
|
||||||
####################### Setup Toolbar #####################
|
####################### Setup Toolbar #####################
|
||||||
ToolbarMixin.__init__(self)
|
ToolbarMixin.__init__(self)
|
||||||
@ -417,6 +410,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.tags_view.set_new_model() # in case columns changed
|
self.tags_view.set_new_model() # in case columns changed
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
self.create_device_menu()
|
self.create_device_menu()
|
||||||
|
self.set_device_menu_items_state(bool(self.device_connected),
|
||||||
|
self.device_connected == 'folder')
|
||||||
|
|
||||||
if not patheq(self.library_path, d.database_location):
|
if not patheq(self.library_path, d.database_location):
|
||||||
newloc = d.database_location
|
newloc = d.database_location
|
||||||
|
@ -136,6 +136,23 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
|
|
||||||
def initialize_dynamic(self):
|
def initialize_dynamic(self):
|
||||||
|
self.conn.executescript('''
|
||||||
|
DROP TRIGGER IF EXISTS author_insert_trg;
|
||||||
|
CREATE TEMP TRIGGER author_insert_trg
|
||||||
|
AFTER INSERT ON authors
|
||||||
|
BEGIN
|
||||||
|
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
|
||||||
|
END;
|
||||||
|
DROP TRIGGER IF EXISTS author_update_trg;
|
||||||
|
CREATE TEMP TRIGGER author_update_trg
|
||||||
|
BEFORE UPDATE ON authors
|
||||||
|
BEGIN
|
||||||
|
UPDATE authors SET sort=author_to_author_sort(NEW.name)
|
||||||
|
WHERE id=NEW.id AND name <> NEW.name;
|
||||||
|
END;
|
||||||
|
''')
|
||||||
|
self.conn.execute(
|
||||||
|
'UPDATE authors SET sort=author_to_author_sort(name) WHERE sort IS NULL')
|
||||||
self.conn.executescript(u'''
|
self.conn.executescript(u'''
|
||||||
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
|
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
|
||||||
id,
|
id,
|
||||||
|
@ -385,28 +385,5 @@ class SchemaUpgrade(object):
|
|||||||
if table.startswith('custom_column_') and link_table in tables:
|
if table.startswith('custom_column_') and link_table in tables:
|
||||||
create_cust_tag_browser_view(table, link_table)
|
create_cust_tag_browser_view(table, link_table)
|
||||||
|
|
||||||
from calibre.ebooks.metadata import author_to_author_sort
|
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
|
||||||
|
|
||||||
aut = self.conn.get('SELECT id, name FROM authors');
|
|
||||||
records = []
|
|
||||||
for (id, author) in aut:
|
|
||||||
records.append((id, author.replace('|', ',')))
|
|
||||||
for id,author in records:
|
|
||||||
self.conn.execute('UPDATE authors SET sort=? WHERE id=?',
|
|
||||||
(author_to_author_sort(author.replace('|', ',')).strip(), id))
|
|
||||||
self.conn.commit()
|
|
||||||
self.conn.executescript('''
|
|
||||||
DROP TRIGGER IF EXISTS author_insert_trg;
|
|
||||||
CREATE TRIGGER author_insert_trg
|
|
||||||
AFTER INSERT ON authors
|
|
||||||
BEGIN
|
|
||||||
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
|
|
||||||
END;
|
|
||||||
DROP TRIGGER IF EXISTS author_update_trg;
|
|
||||||
CREATE TRIGGER author_update_trg
|
|
||||||
BEFORE UPDATE ON authors
|
|
||||||
BEGIN
|
|
||||||
UPDATE authors SET sort=author_to_author_sort(NEW.name)
|
|
||||||
WHERE id=NEW.id AND name <> NEW.name;
|
|
||||||
END;
|
|
||||||
''')
|
|
||||||
|
@ -94,6 +94,9 @@ class Connection(sqlite.Connection):
|
|||||||
return ans[0]
|
return ans[0]
|
||||||
return ans.fetchall()
|
return ans.fetchall()
|
||||||
|
|
||||||
|
def _author_to_author_sort(x):
|
||||||
|
if not x: return ''
|
||||||
|
return author_to_author_sort(x.replace('|', ','))
|
||||||
|
|
||||||
class DBThread(Thread):
|
class DBThread(Thread):
|
||||||
|
|
||||||
@ -121,7 +124,7 @@ class DBThread(Thread):
|
|||||||
else:
|
else:
|
||||||
self.conn.create_function('title_sort', 1, title_sort)
|
self.conn.create_function('title_sort', 1, title_sort)
|
||||||
self.conn.create_function('author_to_author_sort', 1,
|
self.conn.create_function('author_to_author_sort', 1,
|
||||||
lambda x: author_to_author_sort(x.replace('|', ',')))
|
_author_to_author_sort)
|
||||||
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||||
# Dummy functions for dynamically created filters
|
# Dummy functions for dynamically created filters
|
||||||
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
||||||
|
@ -596,10 +596,11 @@ class DNSIncoming(object):
|
|||||||
next = off + 1
|
next = off + 1
|
||||||
off = ((len & 0x3F) << 8) | ord(self.data[off])
|
off = ((len & 0x3F) << 8) | ord(self.data[off])
|
||||||
if off >= first:
|
if off >= first:
|
||||||
raise 'Bad domain name (circular) at ' + str(off)
|
raise ValueError('Bad domain name (circular) at ' +
|
||||||
|
str(off))
|
||||||
first = off
|
first = off
|
||||||
else:
|
else:
|
||||||
raise 'Bad domain name at ' + str(off)
|
raise ValueError('Bad domain name at ' + str(off))
|
||||||
|
|
||||||
if next >= 0:
|
if next >= 0:
|
||||||
self.offset = next
|
self.offset = next
|
||||||
|
@ -788,6 +788,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary_byline {
|
.summary_byline {
|
||||||
|
text-align:left;
|
||||||
font-family:monospace;
|
font-family:monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1139,12 +1140,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
|
||||||
mi.publisher = __appname__
|
mi.publisher = __appname__
|
||||||
mi.author_sort = __appname__
|
mi.author_sort = __appname__
|
||||||
if self.output_profile.name == 'iPad':
|
|
||||||
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
|
|
||||||
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
|
||||||
|