This commit is contained in:
GRiker 2012-07-27 04:57:02 -06:00
commit 5921a12390
67 changed files with 1795 additions and 745 deletions

View File

@ -19,6 +19,50 @@
# new recipes:
# - title:
- version: 0.8.62
date: 2012-07-27
new features:
- title: "Book details panel: Allow right clicking on a format to delete it."
- title: "When errors occur in lots of background jobs, add an option to the error message to temporarily suppress subsequent error messages."
tickets: [886904]
- title: "E-book viewer full screen mode: Allow clicking in the left and right page margins to turn pages."
tickets: [1024819]
- title: "Drivers for various Android devices"
tickets: [1028690,1027431]
- title: "Advanced search dialog: When starting on the title/author/etc. tab, restore the previously used search kind as well."
tickets: [1029745]
- title: "When presenting the calibre must be restarted warning after installing a new plugin, add a restart now button so that the user can conveniently restart calibre. Currently only works when going vie Preferences->Plugins->Get new plugins"
bug fixes:
- title: "Fix main window layout state being saved incorrectly if calibre is killed without a proper shutdown"
- title: "Fix boolean and date searching in non english calibre installs."
- title: "Conversion: Ignore invalid chapter detection and level n ToC expressions instead of erroring out"
improved recipes:
- Psychology Today
- The Smithsonian
- The New Republic
- Various updated Polish news sources
- The Sun
- San Francisco Bay Guardian
- AnandTech
- Smashing Magazine
new recipes:
- title: Linux Journal and Conowego.pl
author: fenuks
- title: A list apart and .net magazine
author: Marc Busque
- version: 0.8.61
date: 2012-07-20

17
manual/develop.rst Executable file → Normal file
View File

@ -151,25 +151,20 @@ calibre is the directory that contains the src and resources sub-directories. En
The next step is to create a bash script that will set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory when running calibre in debug mode.
Create a plain text file:
Create a plain text file::
#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g
Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be run:
Save this file as ``/usr/bin/calibre-develop``, then set its permissions so that it can be executed::
chmod +x /usr/bin/calibre-develop
Once you have done this, type
Once you have done this, type::
calibre-develop
You should see some diagnostic information in the Terminal window as calibre starts up, and you should see an asterisk after the version number in the GUI window, indicating that you are running from source.
That's it! You are now ready to start hacking on the |app| code. For example, open the file :file:`src/calibre/__init__.py`
in your favorite editor and add the line::
print ("Hello, world!")
near the top of the file. Now run the command :command:`calibredb`. The very first line of output should be ``Hello, world!``.
You should see some diagnostic information in the Terminal window as calibre
starts up, and you should see an asterisk after the version number in the GUI
window, indicating that you are running from source.
Linux development environment
------------------------------

View File

@ -21,8 +21,12 @@ class anan(BasicNewsRecipe):
remove_javascript = True
encoding = 'utf-8'
remove_tags=[dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}),
dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'})]
remove_tags=[
dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}),
dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'}),
{'attrs':{'class':['article_links', 'header', 'body_right']}},
{'id':['crumbs']},
]
feeds = [ ('Anandtech', 'http://www.anandtech.com/rss/')]

View File

@ -1,6 +1,6 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Benchmark_pl(BasicNewsRecipe):
class BenchmarkPl(BasicNewsRecipe):
title = u'Benchmark.pl'
__author__ = 'fenuks'
description = u'benchmark.pl -IT site'
@ -14,7 +14,7 @@ class Benchmark_pl(BasicNewsRecipe):
preprocess_regexps = [(re.compile(ur'<h3><span style="font-size: small;">&nbsp;Zobacz poprzednie <a href="http://www.benchmark.pl/news/zestawienie/grupa_id/135">Opinie dnia:</a></span>.*</body>', re.DOTALL|re.IGNORECASE), lambda match: '</body>'), (re.compile(ur'Więcej o .*?</ul>', re.DOTALL|re.IGNORECASE), lambda match: '')]
keep_only_tags=[dict(name='div', attrs={'class':['m_zwykly', 'gallery']})]
remove_tags_after=dict(name='div', attrs={'class':'body'})
remove_tags=[dict(name='div', attrs={'class':['kategoria', 'socialize', 'thumb', 'panelOcenaObserwowane', 'categoryNextToSocializeGallery']}), dict(name='table', attrs={'background':'http://www.benchmark.pl/uploads/backend_img/a/fotki_newsy/opinie_dnia/bg.png'}), dict(name='table', attrs={'width':'210', 'cellspacing':'1', 'cellpadding':'4', 'border':'0', 'align':'right'})]
remove_tags=[dict(name='div', attrs={'class':['kategoria', 'socialize', 'thumb', 'panelOcenaObserwowane', 'categoryNextToSocializeGallery', 'breadcrumb']}), dict(name='table', attrs={'background':'http://www.benchmark.pl/uploads/backend_img/a/fotki_newsy/opinie_dnia/bg.png'}), dict(name='table', attrs={'width':'210', 'cellspacing':'1', 'cellpadding':'4', 'border':'0', 'align':'right'})]
INDEX= 'http://www.benchmark.pl'
feeds = [(u'Aktualności', u'http://www.benchmark.pl/rss/aktualnosci-pliki.xml'),
(u'Testy i recenzje', u'http://www.benchmark.pl/rss/testy-recenzje-minirecenzje.xml')]

38
recipes/conowego_pl.recipe Executable file
View File

@ -0,0 +1,38 @@
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class CoNowegoPl(BasicNewsRecipe):
title = u'conowego.pl'
__author__ = 'fenuks'
description = u'Nowy wortal technologiczny oraz gazeta internetowa. Testy najnowszych produktów, fachowe porady i recenzje. U nas znajdziesz wszystko o elektronice użytkowej !'
cover_url = 'http://www.conowego.pl/fileadmin/templates/main/images/logo_top.png'
category = 'IT, news'
language = 'pl'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
remove_empty_feeds = True
use_embedded_content = False
keep_only_tags = [dict(name='div', attrs={'class':'news_list single_view'})]
remove_tags = [dict(name='div', attrs={'class':['ni_bottom', 'ni_rank', 'ni_date']})]
feeds = [(u'Aktualno\u015bci', u'http://www.conowego.pl/rss/aktualnosci-5/?type=100'), (u'Gaming', u'http://www.conowego.pl/rss/gaming-6/?type=100'), (u'Porady', u'http://www.conowego.pl/rss/porady-3/?type=100'), (u'Testy', u'http://www.conowego.pl/rss/testy-2/?type=100')]
def preprocess_html(self, soup):
for i in soup.findAll('img'):
i.parent.insert(0, BeautifulSoup('<br />'))
i.insert(len(i), BeautifulSoup('<br />'))
self.append_page(soup, soup.body)
return soup
def append_page(self, soup, appendtag):
tag = appendtag.find('div', attrs={'class':'pages'})
if tag:
nexturls=tag.findAll('a')
for nexturl in nexturls[:-1]:
soup2 = self.index_to_soup('http://www.conowego.pl/' + nexturl['href'])
pagetext = soup2.find(attrs={'class':'ni_content'})
pos = len(appendtag.contents)
appendtag.insert(pos, pagetext)
for r in appendtag.findAll(attrs={'class':['pages', 'paginationWrap']}):
r.extract()

32
recipes/dot_net.recipe Normal file
View File

@ -0,0 +1,32 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe
import re
class NetMagazineRecipe (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>'
__url__ = 'http://www.lamarciana.com'
__version__ = '1.0'
__license__ = 'GPL v3'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
title = u'.net magazine'
description = u'net is the worlds best-selling magazine for web designers and developers, featuring tutorials from leading agencies, interviews with the webs biggest names, and agenda-setting features on the hottest issues affecting the internet today.'
language = 'en'
tags = 'web development, software'
oldest_article = 7
remove_empty_feeds = True
no_stylesheets = True
cover_url = u'http://media.netmagazine.futurecdn.net/sites/all/themes/netmag/logo.png'
keep_only_tags = [
dict(name='article', attrs={'class': re.compile('^node.*$', re.IGNORECASE)})
]
remove_tags = [
dict(name='span', attrs={'class': 'comment-count'}),
dict(name='div', attrs={'class': 'item-list share-links'}),
dict(name='footer'),
]
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style']
extra_css = 'img {max-width: 100%; display: block; margin: auto;} .captioned-image div {text-align: center; font-style: italic;}'
feeds = [
(u'.net', u'http://feeds.feedburner.com/net/topstories'),
]

View File

@ -1,6 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe
class Filmweb_pl(BasicNewsRecipe):
import re
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class FilmWebPl(BasicNewsRecipe):
title = u'FilmWeb'
__author__ = 'fenuks'
description = 'FilmWeb - biggest polish movie site'
@ -12,8 +13,9 @@ class Filmweb_pl(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets= True
remove_empty_feeds=True
preprocess_regexps = [(re.compile(u'\(kliknij\,\ aby powiększyć\)', re.IGNORECASE), lambda m: ''), ]#(re.compile(ur' | ', re.IGNORECASE), lambda m: '')]
extra_css = '.hdrBig {font-size:22px;} ul {list-style-type:none; padding: 0; margin: 0;}'
remove_tags= [dict(name='div', attrs={'class':['recommendOthers']}), dict(name='ul', attrs={'class':'fontSizeSet'})]
remove_tags= [dict(name='div', attrs={'class':['recommendOthers']}), dict(name='ul', attrs={'class':'fontSizeSet'}), dict(attrs={'class':'userSurname anno'})]
keep_only_tags= [dict(name='h1', attrs={'class':['hdrBig', 'hdrEntity']}), dict(name='div', attrs={'class':['newsInfo', 'newsInfoSmall', 'reviewContent description']})]
feeds = [(u'Wszystkie newsy', u'http://www.filmweb.pl/feed/news/latest'),
(u'News / Filmy w produkcji', 'http://www.filmweb.pl/feed/news/category/filminproduction'),
@ -31,18 +33,22 @@ class Filmweb_pl(BasicNewsRecipe):
(u'News / Kino polskie', u'http://www.filmweb.pl/feed/news/category/polish.cinema'),
(u'News / Telewizja', u'http://www.filmweb.pl/feed/news/category/tv'),
(u'Recenzje redakcji', u'http://www.filmweb.pl/feed/reviews/latest'),
(u'Recenzje użytkowników', u'http://www.filmweb.pl/feed/user-reviews/latest')]
(u'Recenzje użytkowników', u'http://www.filmweb.pl/feed/user-reviews/latest')
]
def skip_ad_pages(self, soup):
def skip_ad_pages(self, soup):
skip_tag = soup.find('a', attrs={'class':'welcomeScreenButton'})
if skip_tag is not None:
self.log.warn('skip_tag')
self.log.warn(skip_tag)
return self.index_to_soup(skip_tag['href'], raw=True)
def preprocess_html(self, soup):
for a in soup('a'):
if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']:
a['href']=self.index + a['href']
return soup
for i in soup.findAll('a', attrs={'class':'fn'}):
i.insert(len(i), BeautifulSoup('<br />'))
for i in soup.findAll('sup'):
if not i.string or i.string.startswith('(kliknij'):
i.extract()
return soup

View File

@ -1,6 +1,6 @@
from calibre.web.feeds.recipes import BasicNewsRecipe
class Gry_online_pl(BasicNewsRecipe):
class GryOnlinePl(BasicNewsRecipe):
title = u'Gry-Online.pl'
__author__ = 'fenuks'
description = 'Gry-Online.pl - computer games'
@ -21,17 +21,18 @@ class Gry_online_pl(BasicNewsRecipe):
tag = appendtag.find('div', attrs={'class':'n5p'})
if tag:
nexturls=tag.findAll('a')
for nexturl in nexturls[1:]:
try:
soup2 = self.index_to_soup('http://www.gry-online.pl/S020.asp'+ nexturl['href'])
except:
soup2 = self.index_to_soup('http://www.gry-online.pl/S022.asp'+ nexturl['href'])
url_part = soup.find('link', attrs={'rel':'canonical'})['href']
url_part = url_part[25:].rpartition('?')[0]
for nexturl in nexturls[1:-1]:
soup2 = self.index_to_soup('http://www.gry-online.pl/' + url_part + nexturl['href'])
pagetext = soup2.find(attrs={'class':'gc660'})
for r in pagetext.findAll(name='header'):
r.extract()
for r in pagetext.findAll(attrs={'itemprop':'description'}):
r.extract()
pos = len(appendtag.contents)
appendtag.insert(pos, pagetext)
for r in appendtag.findAll(attrs={'class':['n5p', 'add-info', 'twitter-share-button']}):
for r in appendtag.findAll(attrs={'class':['n5p', 'add-info', 'twitter-share-button', 'lista lista3 lista-gry']}):
r.extract()

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

36
recipes/linux_journal.recipe Executable file
View File

@ -0,0 +1,36 @@
from calibre.web.feeds.news import BasicNewsRecipe
class LinuxJournal(BasicNewsRecipe):
title = u'Linux Journal'
__author__ = 'fenuks'
description = u'The monthly magazine of the Linux community, promoting the use of Linux worldwide.'
cover_url = 'http://www.linuxjournal.com/files/linuxjournal.com/ufiles/logo-lj.jpg'
category = 'IT, Linux'
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
remove_empty_feeds = True
keep_only_tags=[dict(id='content-inner')]
remove_tags_after= dict(attrs={'class':'user-signature clear-block'})
remove_tags=[dict(attrs={'class':['user-signature clear-block', 'breadcrumb', 'terms terms-inline']})]
feeds = [(u'Front Page', u'http://feeds.feedburner.com/linuxjournalcom'), (u'News', u'http://feeds.feedburner.com/LinuxJournal-BreakingNews'), (u'Blogs', u'http://www.linuxjournal.com/blog/feed'), (u'Audio/Video', u'http://www.linuxjournal.com/taxonomy/term/28/0/feed'), (u'Community', u'http://www.linuxjournal.com/taxonomy/term/18/0/feed'), (u'Education', u'http://www.linuxjournal.com/taxonomy/term/25/0/feed'), (u'Embedded', u'http://www.linuxjournal.com/taxonomy/term/27/0/feed'), (u'Hardware', u'http://www.linuxjournal.com/taxonomy/term/23/0/feed'), (u'HOWTOs', u'http://www.linuxjournal.com/taxonomy/term/19/0/feed'), (u'International', u'http://www.linuxjournal.com/taxonomy/term/30/0/feed'), (u'Security', u'http://www.linuxjournal.com/taxonomy/term/31/0/feed'), (u'Software', u'http://www.linuxjournal.com/taxonomy/term/17/0/feed'), (u'Sysadmin', u'http://www.linuxjournal.com/taxonomy/term/21/0/feed'), (u'Webmaster', u'http://www.linuxjournal.com/taxonomy/term/24/0/feed')]
def append_page(self, soup, appendtag):
next = appendtag.find('li', attrs={'class':'pager-next'})
while next:
nexturl = next.a['href']
appendtag.find('div', attrs={'class':'links'}).extract()
soup2 = self.index_to_soup('http://www.linuxjournal.com'+ nexturl)
pagetext = soup2.find(attrs={'class':'node-inner'}).find(attrs={'class':'content'})
next = appendtag.find('li', attrs={'class':'pager-next'})
pos = len(appendtag.contents)
appendtag.insert(pos, pagetext)
tag = appendtag.find('div', attrs={'class':'links'})
if tag:
tag.extract()
def preprocess_html(self, soup):
self.append_page(soup, soup.body)
return soup

33
recipes/list_apart.recipe Normal file
View File

@ -0,0 +1,33 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe
class AListApart (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>'
__url__ = 'http://www.lamarciana.com'
__version__ = '1.0'
__license__ = 'GPL v3'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
title = u'A List Apart'
description = u'A List Apart Magazine (ISSN: 1534-0295) explores the design, development, and meaning of web content, with a special focus on web standards and best practices.'
language = 'en'
tags = 'web development, software'
oldest_article = 120
remove_empty_feeds = True
no_stylesheets = True
encoding = 'utf8'
cover_url = u'http://alistapart.com/pix/alalogo.gif'
keep_only_tags = [
dict(name='div', attrs={'id': 'content'})
]
remove_tags = [
dict(name='ul', attrs={'id': 'metastuff'}),
dict(name='div', attrs={'class': 'discuss'}),
dict(name='div', attrs={'class': 'discuss'}),
dict(name='div', attrs={'id': 'learnmore'}),
]
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height']
extra_css = u'img {max-width: 100%; display: block; margin: auto;} #authorbio img {float: left; margin-right: 2%;}'
feeds = [
(u'A List Apart', u'http://www.alistapart.com/site/rss'),
]

View File

@ -1,3 +1,4 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class NaTemat(BasicNewsRecipe):
@ -8,8 +9,9 @@ class NaTemat(BasicNewsRecipe):
description = u'informacje, komentarze, opinie'
category = 'news'
language = 'pl'
preprocess_regexps = [(re.compile(ur'Czytaj też\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Zobacz też\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Czytaj więcej\:.*?</a>', re.IGNORECASE), lambda m: ''), (re.compile(ur'Czytaj również\:.*?</a>', re.IGNORECASE), lambda m: '')]
cover_url= 'http://blog.plona.pl/wp-content/uploads/2012/05/natemat.png'
no_stylesheets = True
keep_only_tags= [dict(id='main')]
remove_tags= [dict(attrs={'class':['button', 'block-inside style_default', 'article-related']})]
remove_tags= [dict(attrs={'class':['button', 'block-inside style_default', 'article-related', 'user-header', 'links']}), dict(name='img', attrs={'class':'indent'})]
feeds = [(u'Artyku\u0142y', u'http://natemat.pl/rss/wszystkie')]

View File

@ -1,44 +1,79 @@
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1275708473(BasicNewsRecipe):
title = u'Psychology Today'
_author__ = 'rty'
publisher = u'www.psychologytoday.com'
category = u'Psychology'
max_articles_per_feed = 100
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
class PsychologyToday(BasicNewsRecipe):
title = 'Psychology Today'
__author__ = 'Rick Shang'
description = 'This magazine takes information from the latest research in the field of psychology and makes it useful to people in their everyday lives. Its coverage encompasses self-improvement, relationships, the mind-body connection, health, family, the workplace and culture.'
language = 'en'
temp_files = []
articles_are_obfuscated = True
remove_tags = [
dict(name='div', attrs={'class':['print-source_url','field-items','print-footer']}),
dict(name='span', attrs={'class':'print-footnote'}),
]
remove_tags_before = dict(name='h1', attrs={'class':'print-title'})
remove_tags_after = dict(name='div', attrs={'class':['field-items','print-footer']})
category = 'news'
encoding = 'UTF-8'
keep_only_tags = [dict(attrs={'class':['print-title', 'print-submitted', 'print-content', 'print-footer', 'print-source_url', 'print-links']})]
no_javascript = True
no_stylesheets = True
feeds = [(u'Contents', u'http://www.psychologytoday.com/articles/index.rss')]
def get_article_url(self, article):
return article.get('link', None)
def parse_index(self):
articles = []
soup = self.index_to_soup('http://www.psychologytoday.com/magazine')
#Go to the main body
div = soup.find('div',attrs={'id':'content-content'})
#Find cover & date
cover_item = div.find('div', attrs={'class':'collections-header-image'})
cover = cover_item.find('img',src=True)
self.cover_url = cover['src']
date = self.tag_to_string(cover['title'])
self.timefmt = u' [%s]'%date
articles = []
for post in div.findAll('div', attrs={'class':'collections-node-feature-info'}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
author = re.sub(r'.*by\s',"",self.tag_to_string(author_item).strip())
title = title + u' (%s)'%author
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
desc = self.tag_to_string(post.find('div', attrs={'class':'collection-node-description'})).strip()
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
for post in div.findAll('div', attrs={'class':'collections-node-thumbnail-info'}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
description = post.find('div', attrs={'class':'collection-node-description'})
author = re.sub(r'.*by\s',"",self.tag_to_string(description.nextSibling).strip())
desc = self.tag_to_string(description).strip()
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
title = title + u' (%s)'%author
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
for post in div.findAll('li', attrs={'class':['collection-item-list-odd','collection-item-list-even']}):
title = self.tag_to_string(post.find('h2'))
author_item=post.find('div', attrs={'class':'collection-node-byline'})
author = re.sub(r'.*by\s',"",self.tag_to_string(author_item).strip())
title = title + u' (%s)'%author
article_page= self.index_to_soup('http://www.psychologytoday.com'+post.find('a', href=True)['href'])
print_page=article_page.find('li', attrs={'class':'print_html first'})
url='http://www.psychologytoday.com'+print_page.find('a',href=True)['href']
desc = self.tag_to_string(post.find('div', attrs={'class':'collection-node-description'})).strip()
self.log('Found article:', title)
self.log('\t', url)
self.log('\t', desc)
articles.append({'title':title, 'url':url, 'date':'','description':desc})
return [('Current Issue', articles)]
def get_obfuscated_article(self, url):
br = self.get_browser()
br.open(url)
response = br.follow_link(url_regex = r'/print/[0-9]+', nr = 0)
html = response.read()
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
self.temp_files[-1].write(html)
self.temp_files[-1].close()
return self.temp_files[-1].name
def get_cover_url(self):
index = 'http://www.psychologytoday.com/magazine/'
soup = self.index_to_soup(index)
for image in soup.findAll('img',{ "class" : "imagefield imagefield-field_magazine_cover" }):
return image['src'] + '.jpg'
return None

View File

@ -1,25 +1,35 @@
from calibre.web.feeds.news import BasicNewsRecipe
class SanFranciscoBayGuardian(BasicNewsRecipe):
title = u'San Francisco Bay Guardian'
language = 'en'
__author__ = 'Krittika Goyal'
title = u'San Francisco Bay Guardian'
language = 'en'
__author__ = 'Krittika Goyal'
oldest_article = 31 #days
max_articles_per_feed = 25
#encoding = 'latin1'
no_stylesheets = True
#remove_tags_before = dict(name='div', attrs={'id':'story_header'})
#remove_tags_after = dict(name='div', attrs={'id':'shirttail'})
remove_tags = [
dict(name='iframe'),
dict(name='iframe'),
#dict(name='div', attrs={'class':'related-articles'}),
#dict(name='div', attrs={'id':['story_tools', 'toolbox', 'shirttail', 'comment_widget']}),
#dict(name='ul', attrs={'class':'article-tools'}),
#dict(name='ul', attrs={'id':'story_tabs'}),
]
feeds = [
('sfbg', 'http://www.sfbg.com/rss.xml'),
('politics', 'http://www.sfbg.com/politics/rss.xml'),
('blogs', 'http://www.sfbg.com/blog/rss.xml'),
('pixel_vision', 'http://www.sfbg.com/pixel_vision/rss.xml'),
('bruce', 'http://www.sfbg.com/bruce/rss.xml'),
]
#def preprocess_html(self, soup):
#story = soup.find(name='div', attrs={'id':'story_body'})
#td = heading.findParent(name='td')
#td.extract()
#soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
#body = soup.find(name='body')
#body.insert(0, story)
#return soup

View File

@ -1,50 +1,24 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
www.smashingmagazine.com
'''
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe
class SmashingMagazine(BasicNewsRecipe):
title = 'Smashing Magazine'
__author__ = 'Darko Miletic'
description = 'We smash you with the information that will make your life easier, really'
oldest_article = 20
language = 'en'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
publisher = 'Smashing Magazine'
category = 'news, web, IT, css, javascript, html'
encoding = 'utf-8'
class SmashingMagazine (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>'
__url__ = 'http://www.lamarciana.com'
__version__ = '1.0.1'
__license__ = 'GPL v3'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>'
title = u'Smashing Magazine'
description = u'Founded in September 2006, Smashing Magazine delivers useful and innovative information to Web designers and developers. Our aim is to inform our readers about the latest trends and techniques in Web development. We try to persuade you not with the quantity but with the quality of the information we present. Smashing Magazine is and always has been independent.'
language = 'en'
tags = 'web development, software'
oldest_article = 7
remove_empty_feeds = True
no_stylesheets = True
encoding = 'utf8'
cover_url = u'http://media.smashingmagazine.com/themes/smashingv4/images/logo.png'
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style']
extra_css = u'body div table:first-child {display: none;} img {max-width: 100%; display: block; margin: auto;}'
conversion_options = {
'comments' : description
,'tags' : category
,'publisher' : publisher
}
keep_only_tags = [dict(name='div', attrs={'id':'leftcolumn'})]
remove_tags_after = dict(name='ul',attrs={'class':'social'})
remove_tags = [
dict(name=['link','object'])
,dict(name='h1',attrs={'class':'logo'})
,dict(name='div',attrs={'id':'booklogosec'})
,dict(attrs={'src':'http://media2.smashingmagazine.com/wp-content/uploads/images/the-smashing-book/smbook6.gif'})
]
feeds = [(u'Articles', u'http://rss1.smashingmagazine.com/feed/')]
def preprocess_html(self, soup):
for iter in soup.findAll('div',attrs={'class':'leftframe'}):
it = iter.find('h1')
if it == None:
iter.extract()
for item in soup.findAll('img'):
oldParent = item.parent
if oldParent.name == 'a':
oldParent.name = 'div'
return soup
feeds = [
(u'Smashing Magazine', u'http://rss1.smashingmagazine.com/feed/'),
]

View File

@ -1,61 +1,67 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.web.feeds.recipes import BasicNewsRecipe
from collections import OrderedDict
class SmithsonianMagazine(BasicNewsRecipe):
title = u'Smithsonian Magazine'
language = 'en'
__author__ = 'Krittika Goyal and TerminalVeracity'
oldest_article = 31#days
max_articles_per_feed = 50
use_embedded_content = False
recursions = 1
cover_url = 'http://sphotos.xx.fbcdn.net/hphotos-snc7/431147_10150602715983253_764313347_n.jpg'
match_regexps = ['&page=[2-9]$']
preprocess_regexps = [
(re.compile(r'for more of Smithsonian\'s coverage on history, science and nature.', re.DOTALL), lambda m: '')
]
extra_css = """
h1{font-size: large; margin: .2em 0}
h2{font-size: medium; margin: .2em 0}
h3{font-size: medium; margin: .2em 0}
#byLine{margin: .2em 0}
.articleImageCaptionwide{font-style: italic}
.wp-caption-text{font-style: italic}
img{display: block}
"""
class Smithsonian(BasicNewsRecipe):
title = 'Smithsonian Magazine'
__author__ = 'Rick Shang'
remove_stylesheets = True
remove_tags_after = dict(name='div', attrs={'class':['post','articlePaginationWrapper']})
remove_tags = [
dict(name='iframe'),
dict(name='div', attrs={'class':['article_sidebar_border','viewMorePhotos','addtoany_share_save_container','meta','social','OUTBRAIN','related-articles-inpage']}),
dict(name='div', attrs={'id':['article_sidebar_border', 'most-popular_large', 'most-popular-body_large','comment_section','article-related']}),
dict(name='ul', attrs={'class':'cat-breadcrumb col three last'}),
dict(name='h4', attrs={'id':'related-topics'}),
dict(name='table'),
dict(name='a', attrs={'href':['/subArticleBottomWeb','/subArticleTopWeb','/subArticleTopMag','/subArticleBottomMag']}),
dict(name='a', attrs={'name':'comments_shaded'}),
]
description = 'This magazine chronicles the arts, environment, sciences and popular culture of the times. It is edited for modern, well-rounded individuals with diverse, general interests. With your order, you become a National Associate Member of the Smithsonian. Membership benefits include your subscription to Smithsonian magazine, a personalized membership card, discounts from the Smithsonian catalog, and more.'
language = 'en'
category = 'news'
encoding = 'UTF-8'
keep_only_tags = [dict(attrs={'id':['articleTitle', 'subHead', 'byLine', 'articleImage', 'article-text']})]
remove_tags = [dict(attrs={'class':['related-articles-inpage', 'viewMorePhotos']})]
no_javascript = True
no_stylesheets = True
def parse_index(self):
#Go to the issue
soup0 = self.index_to_soup('http://www.smithsonianmag.com/issue/archive/')
div = soup0.find('div',attrs={'id':'archives'})
issue = div.find('ul',attrs={'class':'clear-both'})
current_issue_url = issue.find('a', href=True)['href']
soup = self.index_to_soup(current_issue_url)
feeds = [
('History and Archeology',
'http://feeds.feedburner.com/smithsonianmag/history-archaeology'),
('People and Places',
'http://feeds.feedburner.com/smithsonianmag/people-places'),
('Science and Nature',
'http://feeds.feedburner.com/smithsonianmag/science-nature'),
('Arts and Culture',
'http://feeds.feedburner.com/smithsonianmag/arts-culture'),
('Travel',
'http://feeds.feedburner.com/smithsonianmag/travel'),
]
#Go to the main body
div = soup.find ('div', attrs={'id':'content-inset'})
#Find date
date = re.sub('.*\:\W*', "", self.tag_to_string(div.find('h2')).strip())
self.timefmt = u' [%s]'%date
#Find cover
self.cover_url = div.find('img',src=True)['src']
feeds = OrderedDict()
section_title = ''
subsection_title = ''
for post in div.findAll('div', attrs={'class':['plainModule', 'departments plainModule']}):
articles = []
prefix = ''
h3=post.find('h3')
if h3 is not None:
section_title = self.tag_to_string(h3)
else:
subsection=post.find('p',attrs={'class':'article-cat'})
link=post.find('a',href=True)
url=link['href']+'?c=y&story=fullstory'
if subsection is not None:
subsection_title = self.tag_to_string(subsection)
prefix = (subsection_title+': ')
description=self.tag_to_string(post('p', limit=2)[1]).strip()
else:
description=self.tag_to_string(post.find('p')).strip()
desc=re.sub('\sBy\s.*', '', description, re.DOTALL)
author=re.sub('.*By\s', '', description, re.DOTALL)
title=prefix + self.tag_to_string(link).strip()+ u' (%s)'%author
articles.append({'title':title, 'url':url, 'description':desc, 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
return ans
def preprocess_html(self, soup):
story = soup.find(name='div', attrs={'id':'article-body'})
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
body = soup.find(name='body')
body.insert(0, story)
return soup

View File

@ -1,45 +1,64 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
from collections import OrderedDict
class The_New_Republic(BasicNewsRecipe):
title = 'The New Republic'
__author__ = 'cix3'
class TNR(BasicNewsRecipe):
title = 'The New Republic'
__author__ = 'Rick Shang'
description = 'The New Republic is a journal of opinion with an emphasis on politics and domestic and international affairs. It carries feature articles by staff and contributing editors. The second half of each issue is devoted to book and the arts, theater, motion pictures, music and art.'
language = 'en'
description = 'Intelligent, stimulating and rigorous examination of American politics, foreign policy and culture'
timefmt = ' [%b %d, %Y]'
oldest_article = 7
max_articles_per_feed = 100
category = 'news'
encoding = 'UTF-8'
remove_tags = [dict(attrs={'class':['print-logo','print-site_name','print-hr']})]
no_javascript = True
no_stylesheets = True
remove_tags = [
dict(name='div', attrs={'class':['print-logo', 'print-site_name', 'img-left', 'print-source_url']}),
dict(name='hr', attrs={'class':'print-hr'}), dict(name='img')
]
feeds = [
('Politics', 'http://www.tnr.com/rss/articles/Politics'),
('Books and Arts', 'http://www.tnr.com/rss/articles/Books-and-Arts'),
('Economy', 'http://www.tnr.com/rss/articles/Economy'),
('Environment and Energy', 'http://www.tnr.com/rss/articles/Environment-%2526-Energy'),
('Health Care', 'http://www.tnr.com/rss/articles/Health-Care'),
('Metro Policy', 'http://www.tnr.com/rss/articles/Metro-Policy'),
('World', 'http://www.tnr.com/rss/articles/World'),
('Film', 'http://www.tnr.com/rss/articles/Film'),
('Books', 'http://www.tnr.com/rss/articles/books'),
('The Book', 'http://www.tnr.com/rss/book'),
('Jonathan Chait', 'http://www.tnr.com/rss/blogs/Jonathan-Chait'),
('The Plank', 'http://www.tnr.com/rss/blogs/The-Plank'),
('The Treatment', 'http://www.tnr.com/rss/blogs/The-Treatment'),
('The Spine', 'http://www.tnr.com/rss/blogs/The-Spine'),
('The Vine', 'http://www.tnr.com/rss/blogs/The-Vine'),
('The Avenue', 'http://www.tnr.com/rss/blogs/The-Avenue'),
('William Galston', 'http://www.tnr.com/rss/blogs/William-Galston'),
('Simon Johnson', 'http://www.tnr.com/rss/blogs/Simon-Johnson'),
('Ed Kilgore', 'http://www.tnr.com/rss/blogs/Ed-Kilgore'),
('Damon Linker', 'http://www.tnr.com/rss/blogs/Damon-Linker'),
('John McWhorter', 'http://www.tnr.com/rss/blogs/John-McWhorter')
]
def parse_index(self):
def print_version(self, url):
return url.replace('http://www.tnr.com/', 'http://www.tnr.com/print/')
#Go to the issue
soup0 = self.index_to_soup('http://www.tnr.com/magazine-issues')
issue = soup0.find('div',attrs={'id':'current_issue'})
#Find date
date = self.tag_to_string(issue.find('div',attrs={'class':'date'})).strip()
self.timefmt = u' [%s]'%date
#Go to the main body
current_issue_url = 'http://www.tnr.com' + issue.find('a', href=True)['href']
soup = self.index_to_soup(current_issue_url)
div = soup.find ('div', attrs={'class':'article_detail_body'})
#Find cover
self.cover_url = div.find('img',src=True)['src']
feeds = OrderedDict()
section_title = ''
subsection_title = ''
for post in div.findAll('p'):
articles = []
em=post.find('em')
b=post.find('b')
a=post.find('a',href=True)
if em is not None:
section_title = self.tag_to_string(em).strip()
subsection_title = ''
elif b is not None:
subsection_title=self.tag_to_string(b).strip()
elif a is not None:
prefix = (subsection_title+': ') if subsection_title else ''
url=re.sub('www.tnr.com','www.tnr.com/print', a['href'])
author=re.sub('.*by\s', '', self.tag_to_string(post), re.DOTALL)
title=prefix + self.tag_to_string(a).strip()+ u' (%s)'%author
articles.append({'title':title, 'url':url, 'description':'', 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
return ans

View File

@ -1,4 +1,4 @@
import re, random
import random
from calibre import browser
from calibre.web.feeds.recipes import BasicNewsRecipe
@ -8,45 +8,43 @@ class AdvancedUserRecipe1325006965(BasicNewsRecipe):
title = u'The Sun UK'
description = 'Articles from The Sun tabloid UK'
__author__ = 'Dave Asbury'
# last updated 15/7/12
# last updated 25/7/12
language = 'en_GB'
oldest_article = 1
max_articles_per_feed = 15
max_articles_per_feed = 12
remove_empty_feeds = True
no_stylesheets = True
masthead_url = 'http://www.thesun.co.uk/sol/img/global/Sun-logo.gif'
encoding = 'UTF-8'
remove_empty_feeds = True
remove_javascript = True
no_stylesheets = True
#preprocess_regexps = [
# (re.compile(r'<div class="foot-copyright".*?</div>', re.IGNORECASE | re.DOTALL), lambda match: '')]
extra_css = '''
body{ text-align: justify; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal;}
'''
preprocess_regexps = [
(re.compile(r'<div class="foot-copyright".*?</div>', re.IGNORECASE | re.DOTALL), lambda match: '')]
'''
keep_only_tags = [
dict(name='h1'),dict(name='h2',attrs={'class' : ['large','large centered','medium centered','medium']}),dict(name='h3'),
dict(name='div',attrs={'class' : 'text-center'}),
dict(name='div',attrs={'id' : 'bodyText'})
# dict(name='p')
]
remove_tags=[
#dict(name='head'),
dict(attrs={'class' : ['mystery-meat-link','ltbx-container','ltbx-var ltbx-hbxpn','ltbx-var ltbx-nav-loop','ltbx-var ltbx-url']}),
dict(name='div',attrs={'class' : 'cf'}),
dict(attrs={'title' : 'download flash'}),
dict(attrs={'style' : 'padding: 5px'})
dict(name='div',attrs={'class' : 'intro'}),
dict(name='h3'),
dict(name='div',attrs={'id' : 'articlebody'}),
#dict(attrs={'class' : ['right_col_branding','related-stories','mystery-meat-link','ltbx-container','ltbx-var ltbx-hbxpn','ltbx-var ltbx-nav-loop','ltbx-var ltbx-url']}),
# dict(name='div',attrs={'class' : 'cf'}),
# dict(attrs={'title' : 'download flash'}),
# dict(attrs={'style' : 'padding: 5px'})
]
remove_tags_after = [dict(id='bodyText')]
remove_tags=[
dict(name='li'),
dict(attrs={'class' : 'grid-4 right-hand-column'}),
]
feeds = [
(u'News', u'http://www.thesun.co.uk/sol/homepage/news/rss'),

View File

@ -1,7 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class AdvancedUserRecipe1312886443(BasicNewsRecipe):
class WNP(BasicNewsRecipe):
title = u'WNP'
cover_url= 'http://k.wnp.pl/images/wnpLogo.gif'
__author__ = 'fenuks'
@ -12,7 +12,7 @@ class AdvancedUserRecipe1312886443(BasicNewsRecipe):
oldest_article = 8
max_articles_per_feed = 100
no_stylesheets= True
remove_tags=[dict(attrs={'class':'printF'})]
remove_tags=[dict(attrs={'class':['printF', 'border3B2 clearfix', 'articleMenu clearfix']})]
feeds = [(u'Wiadomości gospodarcze', u'http://www.wnp.pl/rss/serwis_rss.xml'),
(u'Serwis Energetyka - Gaz', u'http://www.wnp.pl/rss/serwis_rss_1.xml'),
(u'Serwis Nafta - Chemia', u'http://www.wnp.pl/rss/serwis_rss_2.xml'),

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = u'calibre'
numeric_version = (0, 8, 61)
numeric_version = (0, 8, 62)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -10,7 +10,7 @@ import cStringIO
from calibre.devices.usbms.driver import USBMS
HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229]
HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229, 0x9999]
class ANDROID(USBMS):
@ -41,9 +41,10 @@ class ANDROID(USBMS):
0xca9 : HTC_BCDS,
0xcac : HTC_BCDS,
0xccf : HTC_BCDS,
0xcd6 : HTC_BCDS,
0xce5 : HTC_BCDS,
0x2910 : HTC_BCDS,
0xff9 : HTC_BCDS + [0x9999],
0xff9 : HTC_BCDS,
},
# Eken
@ -194,7 +195,7 @@ class ANDROID(USBMS):
'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON',
'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP',
'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD',
'PMP5097C', 'MASS', 'NOVO7']
'PMP5097C', 'MASS', 'NOVO7', 'ZEKI']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID',
@ -212,7 +213,7 @@ class ANDROID(USBMS):
'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER',
'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX',
'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE',
'ADVANCED']
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -221,7 +222,8 @@ class ANDROID(USBMS):
'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD',
'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD']
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -15,6 +15,8 @@ class DevicePlugin(Plugin):
#: Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
# If True, the config dialog will not show the formats box
HIDE_FORMATS_CONFIG_BOX = False
#: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries,
@ -496,6 +498,92 @@ class DevicePlugin(Plugin):
'''
return paths
def startup(self):
'''
Called when calibre is is starting the device. Do any initialization
required. Note that multiple instances of the class can be instantiated,
and thus __init__ can be called multiple times, but only one instance
will have this method called.
'''
pass
def shutdown(self):
'''
Called when calibre is shutting down, either for good or in preparation
to restart. Do any cleanup required.
'''
pass
# Dynamic control interface.
# The following methods are probably called on the GUI thread. Any driver
# that implements these methods must take pains to be thread safe, because
# the device_manager might be using the driver at the same time that one of
# these methods is called.
def is_dynamically_controllable(self):
'''
Called by the device manager when starting plugins. If this method returns
a string, then a) it supports the device manager's dynamic control
interface, and b) that name is to be used when talking to the plugin.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return None
def start_plugin(self):
'''
This method is called to start the plugin. The plugin should begin
to accept device connections however it does that. If the plugin is
already accepting connections, then do nothing.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def stop_plugin(self):
'''
This method is called to stop the plugin. The plugin should no longer
accept connections, and should cleanup behind itself. It is likely that
this method should call shutdown. If the plugin is already not accepting
connections, then do nothing.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def get_option(self, opt_string, default=None):
'''
Return the value of the option indicated by opt_string. This method can
be called when the plugin is not started. Return None if the option does
not exist.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return default
def set_option(self, opt_string, opt_value):
'''
Set the value of the option indicated by opt_string. This method can
be called when the plugin is not started.
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
pass
def is_running(self):
'''
Return True if the plugin is started, otherwise false
This method can be called on the GUI thread. A driver that implements
this method must be thread safe.
'''
return False
class BookList(list):
'''
A list of books. Each Book object must have the fields

View File

@ -280,17 +280,17 @@ class PRST1(USBMS):
try:
cursor = connection.cursor()
debug_print("Removing Orphaned Collection Records")
# Purge any collections references that point into the abyss
query = 'DELETE FROM collections WHERE content_id NOT IN (SELECT _id FROM books)'
cursor.execute(query)
query = 'DELETE FROM collections WHERE collection_id NOT IN (SELECT _id FROM collection)'
cursor.execute(query)
debug_print("Removing Orphaned Book Records")
# Purge any references to books not in this database
# Idea is to prevent any spill-over where these wind up applying to some other book
query = 'DELETE FROM %s WHERE content_id NOT IN (SELECT _id FROM books)'
@ -301,7 +301,7 @@ class PRST1(USBMS):
cursor.execute(query%'history')
cursor.execute(query%'layout_cache')
cursor.execute(query%'preference')
cursor.close()
except DatabaseError:
import traceback
@ -320,7 +320,7 @@ class PRST1(USBMS):
query = 'SELECT last_insert_rowid()'
cursor.execute(query)
row = cursor.fetchone()
return long(row[0])
def get_database_min_id(self, source_id):
@ -376,6 +376,8 @@ class PRST1(USBMS):
# Record what the max id being used is as well.
db_books = {}
for i, row in enumerate(cursor):
if row[0] is None:
continue
lpath = row[0].replace('\\', '/')
db_books[lpath] = row[1]
if row[1] < sequence_min:

View File

@ -198,11 +198,13 @@ class EPUBInput(InputFormatPlugin):
('application/vnd.adobe-page-template+xml','application/text'):
not_for_spine.add(id_)
seen = set()
for x in list(opf.iterspine()):
ref = x.get('idref', None)
if ref is None or ref in not_for_spine:
if not ref or ref in not_for_spine or ref in seen:
x.getparent().remove(x)
continue
seen.add(ref)
if len(list(opf.iterspine())) == 0:
raise ValueError('No valid entries in the spine of this EPUB')

View File

@ -326,7 +326,7 @@ OptionRecommendation(name='page_breaks_before',
recommended_value="//*[name()='h1' or name()='h2']",
level=OptionRecommendation.LOW,
help=_('An XPath expression. Page breaks are inserted '
'before the specified elements.')
'before the specified elements. To disable use the expression: /')
),
OptionRecommendation(name='remove_fake_margins',

View File

@ -117,8 +117,8 @@ class JsonCodec(object):
def __init__(self):
self.field_metadata = FieldMetadata()
def encode_to_file(self, file, booklist):
file.write(json.dumps(self.encode_booklist_metadata(booklist),
def encode_to_file(self, file_, booklist):
file_.write(json.dumps(self.encode_booklist_metadata(booklist),
indent=2, encoding='utf-8'))
def encode_booklist_metadata(self, booklist):
@ -156,21 +156,28 @@ class JsonCodec(object):
else:
return object_to_unicode(value)
def decode_from_file(self, file, booklist, book_class, prefix):
def decode_from_file(self, file_, booklist, book_class, prefix):
js = []
try:
js = json.load(file, encoding='utf-8')
js = json.load(file_, encoding='utf-8')
for item in js:
book = book_class(prefix, item.get('lpath', None))
for key in item.keys():
meta = self.decode_metadata(key, item[key])
if key == 'user_metadata':
book.set_all_user_metadata(meta)
else:
if key == 'classifiers':
key = 'identifiers'
setattr(book, key, meta)
booklist.append(book)
booklist.append(self.raw_to_book(item, book_class, prefix))
except:
print 'exception during JSON decode_from_file'
traceback.print_exc()
def raw_to_book(self, json_book, book_class, prefix):
try:
book = book_class(prefix, json_book.get('lpath', None))
for key,val in json_book.iteritems():
meta = self.decode_metadata(key, val)
if key == 'user_metadata':
book.set_all_user_metadata(meta)
else:
if key == 'classifiers':
key = 'identifiers'
setattr(book, key, meta)
return book
except:
print 'exception during JSON decoding'
traceback.print_exc()

View File

@ -286,15 +286,17 @@ class Spine(ResourceCollection): # {{{
@staticmethod
def from_opf_spine_element(itemrefs, manifest):
s = Spine(manifest)
seen = set()
for itemref in itemrefs:
idref = itemref.get('idref', None)
if idref is not None:
path = s.manifest.path_for_id(idref)
if path:
if path and path not in seen:
r = Spine.Item(lambda x:idref, path, is_path=True)
r.is_linear = itemref.get('linear', 'yes') == 'yes'
r.idref = idref
s.append(r)
seen.add(path)
return s
@staticmethod

View File

@ -12,7 +12,7 @@ import re
from calibre.ebooks.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS,
namespace, prefixname, urlnormalize)
from calibre.ebooks.mobi.mobiml import MBP_NS
from calibre.ebooks.mobi.utils import is_guide_ref_start
from calibre.ebooks.mobi.utils import is_guide_ref_start, utf8_text
from collections import defaultdict
from urlparse import urldefrag
@ -355,7 +355,7 @@ class Serializer(object):
text = text.replace(u'\u00AD', '') # Soft-hyphen
if quot:
text = text.replace('"', '&quot;')
self.buf.write(text.encode('utf-8'))
self.buf.write(utf8_text(text))
def fixup_links(self):
'''

View File

@ -0,0 +1,65 @@
#!/usr/bin/env coffee
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
###
Copyright 2012, Kovid Goyal <kovid at kovidgoyal.net>
Released under the GPLv3 License
###
log = window.calibre_utils.log
class FullScreen
# This class is a namespace to expose functions via the
# window.full_screen object. The most important functions are:
constructor: () ->
if not this instanceof arguments.callee
throw new Error('FullScreen constructor called as function')
this.in_full_screen = false
this.initial_left_margin = null
this.initial_right_margin = null
save_margins: () ->
bs = document.body.style
this.initial_left_margin = bs.marginLeft
this.initial_right_margin = bs.marginRight
on: (max_text_width, in_paged_mode) ->
if in_paged_mode
window.paged_display.max_col_width = max_text_width
else
s = document.body.style
s.maxWidth = max_text_width + 'px'
s.marginLeft = 'auto'
s.marginRight = 'auto'
window.addEventListener('click', this.handle_click, false)
off: (in_paged_mode) ->
window.removeEventListener('click', this.handle_click, false)
if in_paged_mode
window.paged_display.max_col_width = -1
else
s = document.body.style
s.maxWidth = 'none'
if this.initial_left_margin != null
s.marginLeft = this.initial_left_margin
if this.initial_right_margin != null
s.marginRight = this.initial_right_margin
handle_click: (event) ->
if event.target != document.documentElement or event.button != 0
return
res = null
if window.paged_display.in_paged_mode
res = window.paged_display.click_for_page_turn(event)
else
br = document.body.getBoundingClientRect()
if not (br.left <= event.clientX <= br.right)
res = event.clientX < br.left
if res != null
window.py_bridge.page_turn_requested(res)
if window?
window.full_screen = new FullScreen()

View File

@ -26,6 +26,7 @@ class PagedDisplay
this.current_margin_side = 0
this.is_full_screen_layout = false
this.max_col_width = -1
this.current_page_height = null
this.document_margins = null
this.use_document_margins = false
@ -74,25 +75,12 @@ class PagedDisplay
# start_time = new Date().getTime()
body_style = window.getComputedStyle(document.body)
bs = document.body.style
# When laying body out in columns, webkit bleeds the top margin of the
# first block element out above the columns, leading to an extra top
# margin for the page. We compensate for that here. Computing the
# boundingrect of body is very expensive with column layout, so we do
# it before the column layout is applied.
first_layout = false
if not this.in_paged_mode
bs.setProperty('margin-top', '0px')
extra_margin = document.body.getBoundingClientRect().top
if extra_margin <= this.margin_top
extra_margin = 0
margin_top = (this.margin_top - extra_margin) + 'px'
# Check if the current document is a full screen layout like
# cover, if so we treat it specially.
single_screen = (document.body.scrollWidth < window.innerWidth + 25 and document.body.scrollHeight < window.innerHeight + 25)
first_layout = true
else
# resize event
margin_top = body_style.marginTop
ww = window.innerWidth
@ -116,16 +104,23 @@ class PagedDisplay
col_width = Math.max(100, ((ww - adjust)/n) - 2*sm)
this.page_width = col_width + 2*sm
this.screen_width = this.page_width * this.cols_per_screen
this.current_page_height = window.innerHeight - this.margin_top - this.margin_bottom
fgcolor = body_style.getPropertyValue('color')
bs.setProperty('-webkit-column-gap', (2*sm)+'px')
bs.setProperty('-webkit-column-width', col_width+'px')
bs.setProperty('-webkit-column-rule-color', fgcolor)
# Without this, webkit bleeds the margin of the first block(s) of body
# above the columns, which causes them to effectively be added to the
# page margins (the margin collapse algorithm)
bs.setProperty('-webkit-margin-collapse', 'separate')
bs.setProperty('overflow', 'visible')
bs.setProperty('height', (window.innerHeight - this.margin_top - this.margin_bottom) + 'px')
bs.setProperty('width', (window.innerWidth - 2*sm)+'px')
bs.setProperty('margin-top', margin_top)
bs.setProperty('margin-top', this.margin_top + 'px')
bs.setProperty('margin-bottom', this.margin_bottom+'px')
bs.setProperty('margin-left', sm+'px')
bs.setProperty('margin-right', sm+'px')
@ -167,9 +162,15 @@ class PagedDisplay
# that this method use getBoundingClientRect() which means it will
# force a relayout if the render tree is dirty.
images = []
vimages = []
maxh = this.current_page_height
for img in document.getElementsByTagName('img')
previously_limited = calibre_utils.retrieve(img, 'width-limited', false)
data = calibre_utils.retrieve(img, 'img-data', null)
br = img.getBoundingClientRect()
if data == null
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
calibre_utils.store(img, 'img-data', data)
left = calibre_utils.viewport_to_document(br.left, 0, doc=img.ownerDocument)[0]
col = this.column_at(left) * this.page_width
rleft = left - col - this.current_margin_side
@ -178,23 +179,28 @@ class PagedDisplay
col_width = this.page_width - 2*this.current_margin_side
if previously_limited or rright > col_width
images.push([img, col_width - rleft])
previously_limited = calibre_utils.retrieve(img, 'height-limited', false)
if previously_limited or br.height > maxh
vimages.push(img)
if previously_limited
img.style.setProperty('-webkit-column-break-before', 'auto')
img.style.setProperty('display', data.display)
img.style.setProperty('-webkit-column-break-inside', 'avoid')
for [img, max_width] in images
img.style.setProperty('max-width', max_width+'px')
calibre_utils.store(img, 'width-limited', true)
check_top_margin: () ->
# This is needed to handle the case when a descendant of body specifies
# a top margin as a percentage, which messes up the top margin
# calculations above
tm = document.body.getBoundingClientRect().top
if tm != this.margin_top
document.body.style.setProperty('margin-top', '0px')
tm = document.body.getBoundingClientRect().top
if tm <= this.margin_top
tm = 0
m = this.margin_top - tm
document.body.style.setProperty('margin-top', m+'px')
for img in vimages
data = calibre_utils.retrieve(img, 'img-data', null)
img.style.setProperty('-webkit-column-break-before', 'always')
img.style.setProperty('max-height', maxh+'px')
if data.height > maxh
# This is needed to force the image onto a new page, without
# it, the webkit algorithm may still decide to split the image
# by keeping it part of its parent block
img.style.setProperty('display', 'block')
calibre_utils.store(img, 'height-limited', true)
scroll_to_pos: (frac) ->
# Scroll to the position represented by frac (number between 0 and 1)
@ -395,6 +401,18 @@ class PagedDisplay
log('Viewport cfi:', ans)
return ans
click_for_page_turn: (event) ->
# Check if the click event event should generate a apge turn. Returns
# null if it should not, true if it is a backwards page turn, false if
# it is a forward apge turn.
left_boundary = this.current_margin_side
right_bondary = this.screen_width - this.current_margin_side
if left_boundary > event.clientX
return true
if right_bondary < event.clientX
return false
return null
if window?
window.paged_display = new PagedDisplay()

View File

@ -82,10 +82,17 @@ class DetectStructure(object):
def detect_chapters(self):
self.detected_chapters = []
def find_matches(expr, doc):
try:
return XPath(expr)(doc)
except:
self.log.warn('Invalid chapter expression, ignoring: %s'%expr)
return []
if self.opts.chapter:
chapter_xpath = XPath(self.opts.chapter)
for item in self.oeb.spine:
for x in chapter_xpath(item.data):
for x in find_matches(self.opts.chapter, item.data):
self.detected_chapters.append((item, x))
chapter_mark = self.opts.chapter_mark
@ -164,11 +171,19 @@ class DetectStructure(object):
added = OrderedDict()
added2 = OrderedDict()
counter = 1
def find_matches(expr, doc):
try:
return XPath(expr)(doc)
except:
self.log.warn('Invalid ToC expression, ignoring: %s'%expr)
return []
for document in self.oeb.spine:
previous_level1 = list(added.itervalues())[-1] if added else None
previous_level2 = list(added2.itervalues())[-1] if added2 else None
for elem in XPath(self.opts.level1_toc)(document.data):
for elem in find_matches(self.opts.level1_toc, document.data):
text, _href = self.elem_to_link(document, elem, counter)
counter += 1
if text:
@ -178,7 +193,7 @@ class DetectStructure(object):
#node.add(_('Top'), _href)
if self.opts.level2_toc is not None and added:
for elem in XPath(self.opts.level2_toc)(document.data):
for elem in find_matches(self.opts.level2_toc, document.data):
level1 = None
for item in document.data.iterdescendants():
if item in added:
@ -196,7 +211,8 @@ class DetectStructure(object):
break
if self.opts.level3_toc is not None and added2:
for elem in XPath(self.opts.level3_toc)(document.data):
for elem in find_matches(self.opts.level3_toc,
document.data):
level2 = None
for item in document.data.iterdescendants():
if item in added2:

View File

@ -202,7 +202,6 @@ class PDFWriter(QObject): # {{{
paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout();
paged_display.fit_images();
paged_display.check_top_margin();
''')
mf = self.view.page().mainFrame()
while True:
@ -221,7 +220,7 @@ class PDFWriter(QObject): # {{{
self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts')
def insert_cover(self):
if self.cover_data is None:
if not isinstance(self.cover_data, bytes):
return
item_path = os.path.join(self.tmp_path, 'cover.pdf')
printer = get_pdf_printer(self.opts, output_file_name=item_path,

View File

@ -139,6 +139,21 @@ class DeleteAction(InterfaceAction):
return set([])
return set(map(self.gui.library_view.model().id, rows))
def remove_format_by_id(self, book_id, fmt):
title = self.gui.current_db.title(book_id, index_is_id=True)
if not confirm('<p>'+(_(
'The %(fmt)s format will be <b>permanently deleted</b> from '
'%(title)s. Are you sure?')%dict(fmt=fmt, title=title))
+'</p>', 'library_delete_specific_format', self.gui):
return
self.gui.library_view.model().db.remove_format(book_id, fmt,
index_is_id=True, notify=False)
self.gui.library_view.model().refresh_ids([book_id])
self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(),
self.gui.library_view.currentIndex())
self.gui.tags_view.recount()
def delete_selected_formats(self, *args):
ids = self._get_selected_ids()
if not ids:

View File

@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config
from calibre.constants import iswindows, isosx
from calibre.customize.ui import is_disabled
from calibre.devices.bambook.driver import BAMBOOK
from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog
from calibre.gui2 import info_dialog
class ShareConnMenu(QMenu): # {{{
@ -24,6 +25,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal()
toggle_server = pyqtSignal()
control_smartdevice = pyqtSignal()
dont_add_to = frozenset(['context-menu-device'])
def __init__(self, parent=None):
@ -56,6 +58,11 @@ class ShareConnMenu(QMenu): # {{{
_('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit())
self.control_smartdevice_action = \
self.addAction(QIcon(I('dot_green.png')),
_('Control Smart Device Connections'))
self.control_smartdevice_action.triggered.connect(lambda x:
self.control_smartdevice.emit())
self.addSeparator()
self.email_actions = []
@ -80,6 +87,9 @@ class ShareConnMenu(QMenu): # {{{
text = _('Stop Content Server') + ' [%s]'%get_external_ip()
self.toggle_server_action.setText(text)
def hide_smartdevice_menus(self):
self.control_smartdevice_action.setVisible(False)
def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction
for ac in self.email_actions:
@ -158,6 +168,7 @@ class ConnectShareAction(InterfaceAction):
def genesis(self):
self.share_conn_menu = ShareConnMenu(self.gui)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice)
self.share_conn_menu.config_email.connect(partial(
self.gui.iactions['Preferences'].do_config,
initial_plugin=('Sharing', 'Email')))
@ -200,8 +211,21 @@ class ConnectShareAction(InterfaceAction):
if not self.stopping_msg.isVisible():
self.stopping_msg.exec_()
return
self.gui.content_server = None
self.stopping_msg.accept()
def control_smartdevice(self):
sd_dialog = SmartdeviceDialog(self.gui)
sd_dialog.exec_()
self.set_smartdevice_icon()
def check_smartdevice_menus(self):
if not self.gui.device_manager.is_enabled('smartdevice'):
self.share_conn_menu.hide_smartdevice_menus()
def set_smartdevice_icon(self):
running = self.gui.device_manager.is_running('smartdevice')
if running:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png')))
else:
self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png')))

View File

@ -31,3 +31,5 @@ class PluginUpdaterAction(InterfaceAction):
d = PluginUpdaterDialog(self.gui, initial_filter=initial_filter)
d.exec_()
if d.do_restart:
self.gui.quit(restart=True)

View File

@ -45,6 +45,8 @@ class PreferencesAction(InterfaceAction):
d = PluginUpdaterDialog(self.gui,
initial_filter=FILTER_NOT_INSTALLED)
d.exec_()
if d.do_restart:
self.gui.quit(restart=True)
def do_config(self, checked=False, initial_plugin=None,
close_after_initial=False):

View File

@ -5,8 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo,
from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon,
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction,
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu)
from PyQt4.QtWebKit import QWebView
@ -382,6 +382,7 @@ class CoverView(QWidget): # {{{
class BookInfo(QWebView):
link_clicked = pyqtSignal(object)
remove_format = pyqtSignal(int, object)
def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent)
@ -395,6 +396,16 @@ class BookInfo(QWebView):
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
self.css = P('templates/book_details.css', data=True).decode('utf-8')
self.remove_format_action = QAction(QIcon(I('trash.png')),
'', self)
self.remove_format_action.current_fmt = None
self.remove_format_action.triggered.connect(self.remove_format_triggerred)
def remove_format_triggerred(self):
f = self.remove_format_action.current_fmt
if f:
book_id, fmt = f
self.remove_format.emit(book_id, fmt)
def link_activated(self, link):
self._link_clicked = True
@ -420,6 +431,32 @@ class BookInfo(QWebView):
else:
ev.ignore()
def contextMenuEvent(self, ev):
p = self.page()
mf = p.mainFrame()
r = mf.hitTestContent(ev.pos())
url = unicode(r.linkUrl().toString()).strip()
menu = p.createStandardContextMenu()
ca = self.pageAction(p.Copy)
for action in list(menu.actions()):
if action is not ca:
menu.removeAction(action)
if not r.isNull() and url.startswith('format:'):
parts = url.split(':')
try:
book_id, fmt = int(parts[1]), parts[2]
except:
import traceback
traceback.print_exc()
else:
self.remove_format_action.current_fmt = (book_id, fmt)
self.remove_format_action.setText(_('Delete the %s format')%parts[
2])
menu.addAction(self.remove_format_action)
if len(menu.actions()) > 0:
menu.exec_(ev.globalPos())
# }}}
class DetailsLayout(QLayout): # {{{
@ -513,6 +550,7 @@ class BookDetails(QWidget): # {{{
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
remove_specific_format = pyqtSignal(int, object)
remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object)
@ -579,6 +617,7 @@ class BookDetails(QWidget): # {{{
self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click)
self.book_info.remove_format.connect(self.remove_specific_format)
self.setCursor(Qt.PointingHandCursor)
def handle_click(self, link):

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
# Imports {{{
import os, traceback, Queue, time, cStringIO, re, sys
from threading import Thread
from threading import Thread, Event
from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
Qt, pyqtSignal, QDialog, QObject, QVBoxLayout,
@ -144,6 +144,9 @@ class DeviceManager(Thread): # {{{
self.open_feedback_msg = open_feedback_msg
self._device_information = None
self.current_library_uuid = None
self.call_shutdown_on_disconnect = False
self.devices_initialized = Event()
self.dynamic_plugins = {}
def report_progress(self, *args):
pass
@ -197,6 +200,13 @@ class DeviceManager(Thread): # {{{
self.ejected_devices.remove(self.connected_device)
else:
self.connected_slot(False, self.connected_device_kind)
if self.call_shutdown_on_disconnect:
# The current device is an instance of a plugin class instantiated
# to handle this connection, probably as a mounted device. We are
# now abandoning the instance that we created, so we tell it that it
# is being shut down.
self.connected_device.shutdown()
self.call_shutdown_on_disconnect = False
self.connected_device = None
self._device_information = None
@ -265,7 +275,24 @@ class DeviceManager(Thread): # {{{
except Queue.Empty:
pass
def run_startup(self, dev):
name = 'unknown'
try:
name = dev.__class__.__name__
dev.startup()
except:
prints('Startup method for device %s threw exception'%name)
traceback.print_exc()
def run(self):
# Do any device-specific startup processing.
for d in self.devices:
self.run_startup(d)
n = d.is_dynamically_controllable()
if n:
self.dynamic_plugins[n] = d
self.devices_initialized.set()
while self.keep_going:
kls = None
while True:
@ -277,15 +304,23 @@ class DeviceManager(Thread): # {{{
if kls is not None:
try:
dev = kls(folder_path)
# We just created a new device instance. Call its startup
# method and set the flag to call the shutdown method when
# it disconnects.
self.run_startup(dev)
self.call_shutdown_on_disconnect = True
self.do_connect([[dev, None],], device_kind=device_kind)
except:
prints('Unable to open %s as device (%s)'%(device_kind, folder_path))
traceback.print_exc()
else:
self.detect_device()
do_sleep = True
while True:
job = self.next()
if job is not None:
do_sleep = False
self.current_job = job
if self.device is not None:
self.device.set_progress_reporter(job.report_progress)
@ -293,7 +328,15 @@ class DeviceManager(Thread): # {{{
self.current_job = None
else:
break
time.sleep(self.sleep_time)
if do_sleep:
time.sleep(self.sleep_time)
# We are exiting. Call the shutdown method for each plugin
for p in self.devices:
try:
p.shutdown()
except:
pass
def create_job_step(self, func, done, description, to_job, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager,
@ -475,6 +518,44 @@ class DeviceManager(Thread): # {{{
if self.connected_device:
self.connected_device.set_driveinfo_name(location_code, name)
# dynamic plugin interface
# This is a helper function that handles queueing with the device manager
def _call_request(self, name, method, *args, **kwargs):
d = self.dynamic_plugins.get(name, None)
if d:
return getattr(d, method)(*args, **kwargs)
return kwargs.get('default', None)
# The dynamic plugin methods below must be called on the GUI thread. They
# will switch to the device thread before calling the plugin.
def start_plugin(self, name):
self._call_request(name, 'start_plugin')
def stop_plugin(self, name):
self._call_request(name, 'stop_plugin')
def get_option(self, name, opt_string, default=None):
return self._call_request(name, 'get_option', opt_string, default=default)
def set_option(self, name, opt_string, opt_value):
self._call_request(name, 'set_option', opt_string, opt_value)
def is_running(self, name):
if self._call_request(name, 'is_running'):
return True
return False
def is_enabled(self, name):
try:
d = self.dynamic_plugins.get(name, None)
if d:
return True
except:
pass
return False
# }}}
class DeviceAction(QAction): # {{{
@ -675,6 +756,7 @@ class DeviceMixin(object): # {{{
self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback))
self.device_manager.start()
self.device_manager.devices_initialized.wait()
if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder'])

View File

@ -43,6 +43,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
if device.HIDE_FORMATS_CONFIG_BOX:
self.groupBox.hide()
if supports_subdirs:
self.opt_use_subdirs.setChecked(self.settings.use_subdirs)
else:

View File

@ -103,6 +103,19 @@
<item row="6" column="0">
<layout class="QGridLayout" name="extra_layout"/>
</item>
<item row="7" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">

View File

@ -1,7 +1,8 @@
<ui version="4.0" >
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
@ -9,51 +10,63 @@
<height>300</height>
</rect>
</property>
<property name="windowTitle" >
<property name="windowTitle">
<string>Are you sure?</string>
</property>
<property name="windowIcon" >
<iconset resource="../../../../resources/images.qrc" >
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/dialog_warning.png</normaloff>:/images/dialog_warning.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout" >
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label" >
<property name="pixmap" >
<pixmap resource="../../../../resources/images.qrc" >:/images/dialog_warning.png</pixmap>
<widget class="QLabel" name="label">
<property name="pixmap">
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="msg" >
<property name="text" >
<widget class="QLabel" name="msg">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap" >
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="again" >
<property name="text" >
<item row="1" column="0">
<widget class="QCheckBox" name="again">
<property name="text">
<string>&amp;Show this warning again</string>
</property>
<property name="checked" >
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<item row="2" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
@ -61,7 +74,7 @@
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc" />
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
@ -70,11 +83,11 @@
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
@ -86,11 +99,11 @@
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>

View File

@ -9,7 +9,7 @@ import sys
from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence,
QAction, Qt, QTextBrowser, QDialogButtonBox, QVBoxLayout, QGridLayout,
QLabel, QPlainTextEdit, QTextDocument)
QLabel, QPlainTextEdit, QTextDocument, QCheckBox, pyqtSignal)
from calibre.constants import __version__, isfrozen
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
@ -270,21 +270,23 @@ class ErrorNotification(MessageBox): # {{{
class JobError(QDialog): # {{{
WIDTH = 600
do_pop = pyqtSignal()
def __init__(self, gui):
QDialog.__init__(self, gui)
def __init__(self, parent):
QDialog.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose, False)
self.gui = gui
self.queue = []
self.do_pop.connect(self.pop, type=Qt.QueuedConnection)
self._layout = l = QGridLayout()
self.setLayout(l)
self.icon = QIcon(I('dialog_error.png'))
self.setWindowIcon(self.icon)
self.icon_label = QLabel()
self.icon_label.setPixmap(self.icon.pixmap(128, 128))
self.icon_label.setMaximumSize(QSize(128, 128))
self.icon_label.setPixmap(self.icon.pixmap(68, 68))
self.icon_label.setMaximumSize(QSize(68, 68))
self.msg_label = QLabel('<p>&nbsp;')
self.msg_label.setStyleSheet('QLabel { margin-top: 1ex; }')
self.msg_label.setWordWrap(True)
self.msg_label.setTextFormat(Qt.RichText)
self.det_msg = QPlainTextEdit(self)
@ -302,15 +304,23 @@ class JobError(QDialog): # {{{
self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
self.det_msg_toggle.setToolTip(
_('Show detailed information about this error'))
self.suppress = QCheckBox(self)
l.addWidget(self.icon_label, 0, 0, 1, 1)
l.addWidget(self.msg_label, 0, 1, 1, 1, Qt.AlignLeft|Qt.AlignTop)
l.addWidget(self.det_msg, 1, 0, 1, 2)
l.addWidget(self.bb, 2, 0, 1, 2, Qt.AlignRight|Qt.AlignBottom)
l.addWidget(self.msg_label, 0, 1, 1, 1)
l.addWidget(self.det_msg, 1, 0, 1, 2)
l.addWidget(self.suppress, 2, 0, 1, 2, Qt.AlignLeft|Qt.AlignBottom)
l.addWidget(self.bb, 3, 0, 1, 2, Qt.AlignRight|Qt.AlignBottom)
l.setColumnStretch(1, 100)
self.setModal(False)
self.base_height = max(200, self.sizeHint().height() + 20)
self.suppress.setVisible(False)
self.do_resize()
def update_suppress_state(self):
self.suppress.setText(_(
'Hide the remaining %d error messages'%len(self.queue)))
self.suppress.setVisible(len(self.queue) > 3)
self.do_resize()
def copy_to_clipboard(self, *args):
@ -332,9 +342,11 @@ class JobError(QDialog): # {{{
self.do_resize()
def do_resize(self):
h = self.base_height
if self.det_msg.isVisible():
h += 250
h = self.sizeHint().height()
self.setMinimumHeight(0) # Needed as this gets set if det_msg is shown
# Needed otherwise re-showing the box after showing det_msg causes the box
# to not reduce in height
self.setMaximumHeight(h)
self.resize(QSize(self.WIDTH, h))
def showEvent(self, ev):
@ -342,16 +354,50 @@ class JobError(QDialog): # {{{
self.bb.button(self.bb.Close).setFocus(Qt.OtherFocusReason)
return ret
def show_error(self, title, msg, det_msg=u''):
self.queue.append((title, msg, det_msg))
self.update_suppress_state()
self.pop()
def pop(self):
if not self.queue or self.isVisible(): return
title, msg, det_msg = self.queue.pop(0)
self.setWindowTitle(title)
self.msg_label.setText(msg)
self.det_msg.setPlainText(det_msg)
self.det_msg.setVisible(False)
self.det_msg_toggle.setText(self.show_det_msg)
self.det_msg_toggle.setVisible(True)
self.suppress.setChecked(False)
self.update_suppress_state()
if not det_msg:
self.det_msg_toggle.setVisible(False)
self.do_resize()
self.show()
def done(self, r):
if self.suppress.isChecked():
self.queue = []
QDialog.done(self, r)
self.do_pop.emit()
# }}}
if __name__ == '__main__':
app = QApplication([])
from calibre.gui2.preferences import init_gui
gui = init_gui()
d = JobError(gui)
d.show()
d = JobError(None)
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
d.show_error('test title', 'some long meaningless test message', 'det msg')
app.setQuitOnLastWindowClosed(False)
def checkd():
if not d.queue:
app.quit()
app.lastWindowClosed.connect(checkd)
app.exec_()
gui.shutdown()
# if __name__ == '__main__':
# app = QApplication([])

View File

@ -456,6 +456,7 @@ class PluginUpdaterDialog(SizePersistedDialog):
self.gui = gui
self.forum_link = None
self.model = None
self.do_restart = False
self._initialize_controls()
self._create_context_menu()
@ -720,6 +721,7 @@ class PluginUpdaterDialog(SizePersistedDialog):
prints('Installing plugin: ', zip_path)
self.gui.status_bar.showMessage(_('Installing plugin: %s') % zip_path)
do_restart = False
try:
try:
plugin = add_plugin(zip_path)
@ -731,11 +733,21 @@ class PluginUpdaterDialog(SizePersistedDialog):
widget.gui = self.gui
widget.check_for_add_to_toolbars(plugin)
self.gui.status_bar.showMessage(_('Plugin installed: %s') % display_plugin.name)
info_dialog(self.gui, _('Success'),
d = info_dialog(self.gui, _('Success'),
_('Plugin <b>{0}</b> successfully installed under <b>'
' {1} plugins</b>. You may have to restart calibre '
'for the plugin to take effect.').format(plugin.name, plugin.type),
show=True, show_copy_button=False)
show_copy_button=False)
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
b.setIcon(QIcon(I('lt.png')))
d.do_restart = False
def rf():
d.do_restart = True
b.clicked.connect(rf)
d.set_details('')
d.exec_()
b.clicked.disconnect()
do_restart = d.do_restart
display_plugin.plugin = plugin
# We cannot read the 'actual' version information as the plugin will not be loaded yet
@ -762,6 +774,9 @@ class PluginUpdaterDialog(SizePersistedDialog):
else:
self.model.refresh_plugin(display_plugin)
self._select_and_focus_view(change_selection=False)
if do_restart:
self.do_restart = True
self.accept()
def _history_clicked(self):
display_plugin = self._selected_display_plugin()

View File

@ -12,6 +12,7 @@ from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
box_values = {}
last_matchkind = CONTAINS_MATCH
class SearchDialog(QDialog, Ui_Dialog):
@ -57,6 +58,9 @@ class SearchDialog(QDialog, Ui_Dialog):
current_tab = gprefs.get('advanced search dialog current tab', 0)
self.tabWidget.setCurrentIndex(current_tab)
if current_tab == 1:
self.matchkind.setCurrentIndex(last_matchkind)
self.tabWidget.currentChanged[int].connect(self.tab_changed)
self.tab_changed(current_tab)
@ -173,7 +177,9 @@ class SearchDialog(QDialog, Ui_Dialog):
general_index = unicode(self.general_combo.currentText())
self.box_last_values['general_index'] = general_index
global box_values
global last_matchkind
box_values = copy.deepcopy(self.box_last_values)
last_matchkind = mk
if general:
ans.append(unicode(self.general_combo.currentText()) + ':"' +
self.mc + general + '"')

View File

@ -0,0 +1,80 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtGui import QDialog, QLineEdit
from PyQt4.QtCore import SIGNAL, Qt
from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog
class SmartdeviceDialog(QDialog, Ui_Dialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.msg.setText(
_('This dialog starts and stops the smart device app interface. '
'When you start the interface, you might see some messages from '
'your computer\'s firewall or anti-virus manager asking you '
'if it is OK for calibre to connect to the network. <B>Please '
'answer yes</b>. If you do not, the app will not work. It will '
'be unable to connect to calibre.'))
self.password_box.setToolTip('<p>' +
_('Use a password if calibre is running on a network that '
'is not secure. For example, if you run calibre on a laptop, '
'use that laptop in an airport, and want to connect your '
'smart device to calibre, you should use a password.') + '</p>')
self.run_box.setToolTip('<p>' +
_('Check this box to allow calibre to accept connections from the '
'smart device. Uncheck the box to prevent connections.') + '</p>')
self.autostart_box.setToolTip('<p>' +
_('Check this box if you want calibre to automatically start the '
'smart device interface when calibre starts. You should not do '
'this if you are using a network that is not secure and you '
'are not setting a password.') + '</p>')
self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password)
self.autostart_box.stateChanged.connect(self.autostart_changed)
self.device_manager = parent.device_manager
if self.device_manager.is_running('smartdevice'):
self.run_box.setChecked(True)
else:
self.run_box.setChecked(False)
if self.device_manager.get_option('smartdevice', 'autostart'):
self.autostart_box.setChecked(True)
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
pw = self.device_manager.get_option('smartdevice', 'password')
if pw:
self.password_box.setText(pw)
def autostart_changed(self):
if self.autostart_box.isChecked():
self.run_box.setChecked(True)
self.run_box.setEnabled(False)
else:
self.run_box.setEnabled(True)
def toggle_password(self, state):
if state == Qt.Unchecked:
self.password_box.setEchoMode(QLineEdit.Password)
else:
self.password_box.setEchoMode(QLineEdit.Normal)
def accept(self):
self.device_manager.set_option('smartdevice', 'password',
unicode(self.password_box.text()))
self.device_manager.set_option('smartdevice', 'autostart',
self.autostart_box.isChecked())
if self.run_box.isChecked():
self.device_manager.start_plugin('smartdevice')
else:
self.device_manager.stop_plugin('smartdevice')
QDialog.accept(self)

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>209</height>
</rect>
</property>
<property name="windowTitle">
<string>Smart device control</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
</property>
<layout class="QGridLayout">
<item row="4" column="1">
<widget class="QCheckBox" name="autostart_box">
<property name="text">
<string>&amp;Automatically allow connections at startup</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_43">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Optional password for security</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="run_box">
<property name="text">
<string>&amp;Allow connections</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="msg">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Password:</string>
</property>
<property name="buddy">
<cstring>password_box</cstring>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="show_password">
<property name="text">
<string>&amp;Show password</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="3">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -265,6 +265,8 @@ class LayoutMixin(object): # {{{
type=Qt.QueuedConnection)
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)
self.book_details.remove_specific_format.connect(
self.iactions['Remove Books'].remove_format_by_id)
m = self.library_view.model()
if m.rowCount(None) > 0:

View File

@ -135,7 +135,8 @@ class GuiRunner(QObject):
main = Main(self.opts, gui_debug=self.gui_debug)
if self.splash_screen is not None:
self.splash_screen.showMessage(_('Initializing user interface...'))
main.initialize(self.library_path, db, self.listener, self.actions)
with gprefs: # Only write gui.json after initialization is complete
main.initialize(self.library_path, db, self.listener, self.actions)
if self.splash_screen is not None:
self.splash_screen.finish(main)
if DEBUG:

View File

@ -236,6 +236,7 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
'''
changed_signal = pyqtSignal()
restart_now = pyqtSignal()
supports_restoring_to_defaults = True
restart_critical = False

View File

@ -290,6 +290,7 @@ class Preferences(QMainWindow):
self.apply_action.setEnabled(False)
self.showing_widget.changed_signal.connect(lambda :
self.apply_action.setEnabled(True))
self.showing_widget.restart_now.connect(self.restart_now)
self.restore_action.setEnabled(self.showing_widget.supports_restoring_to_defaults)
tt = self.showing_widget.restore_defaults_desc
if not self.restore_action.isEnabled():
@ -319,6 +320,15 @@ class Preferences(QMainWindow):
elif self.stack.currentIndex() == 0:
self.close()
def restart_now(self):
try:
self.showing_widget.commit()
except AbortCommit:
return
self.hide_plugin()
self.close()
self.gui.quit(restart=True)
def commit(self, *args):
try:
must_restart = self.showing_widget.commit()

View File

@ -384,6 +384,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self._plugin_model.populate()
self._plugin_model.reset()
self.changed_signal.emit()
if d.do_restart:
self.restart_now.emit()
def reload_store_plugins(self):
self.gui.load_store_plugins()

View File

@ -44,6 +44,7 @@ from calibre.gui2.keyboard import Manager
from calibre.gui2.auto_add import AutoAdder
from calibre.library.sqlite import sqlite, DatabaseException
from calibre.gui2.proceed import ProceedQuestion
from calibre.gui2.dialogs.message_box import JobError
class Listener(Thread): # {{{
@ -111,6 +112,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.proceed_requested.connect(self.do_proceed,
type=Qt.QueuedConnection)
self.proceed_question = ProceedQuestion(self)
self.job_error_dialog = JobError(self)
self.keyboard = Manager(self)
_gui = self
self.opts = opts
@ -337,6 +339,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
if config['autolaunch_server']:
self.start_content_server()
smartdevice_actions = self.iactions['Connect Share']
smartdevice_actions.check_smartdevice_menus()
if self.device_manager.get_option('smartdevice', 'autostart'):
try:
self.device_manager.start_plugin('smartdevice')
except:
pass
smartdevice_actions.set_smartdevice_icon()
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
self.read_settings()
@ -358,6 +369,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.keyboard.finalize()
self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)
self.save_layout_state()
# Collect cycles now
gc.collect()
@ -679,12 +692,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except:
pass
if not minz:
d = error_dialog(self, dialog_title,
self.job_error_dialog.show_error(dialog_title,
_('<b>Failed</b>')+': '+unicode(job.description),
det_msg=job.details)
d.setModal(False)
d.show()
self._modeless_dialogs.append(d)
def read_settings(self):
geometry = config['main_window_geometry']
@ -693,9 +703,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.read_layout_settings()
def write_settings(self):
config.set('main_window_geometry', self.saveGeometry())
dynamic.set('sort_history', self.library_view.model().sort_history)
self.save_layout_state()
with gprefs: # Only write to gprefs once
config.set('main_window_geometry', self.saveGeometry())
dynamic.set('sort_history', self.library_view.model().sort_history)
self.save_layout_state()
def quit(self, checked=True, restart=False, debug_on_restart=False,
confirm_quit=True):

View File

@ -175,6 +175,8 @@ class UpdateMixin(object):
d = PluginUpdaterDialog(self,
initial_filter=FILTER_UPDATE_AVAILABLE)
d.exec_()
if d.do_restart:
self.quit(restart=True)
def plugin_update_found(self, number_of_updates):
# Change the plugin icon to indicate there are updates available

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty,
QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint,
QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString,
pyqtSignal, QSwipeGesture, QApplication)
pyqtSignal, QSwipeGesture, QApplication, pyqtSlot)
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.gui2.viewer.flip import SlideFlip
@ -34,6 +34,8 @@ def load_builtin_fonts():
class Document(QWebPage): # {{{
page_turn = pyqtSignal(object)
def set_font_settings(self):
opts = config().parse()
settings = self.settings()
@ -73,7 +75,6 @@ class Document(QWebPage): # {{{
self.loaded_javascript = False
self.js_loader = JavaScriptLoader(
dynamic_coffeescript=self.debug_javascript)
self.initial_left_margin = self.initial_right_margin = u''
self.in_fullscreen_mode = False
self.setLinkDelegationPolicy(self.DelegateAllLinks)
@ -172,6 +173,10 @@ class Document(QWebPage): # {{{
if not isxp and self.hyphenate and getattr(self, 'loaded_lang', ''):
self.javascript('do_hyphenation("%s")'%self.loaded_lang)
@pyqtSlot(int)
def page_turn_requested(self, backwards):
self.page_turn.emit(bool(backwards))
def _pass_json_value_getter(self):
val = json.dumps(self.bridge_value)
return QString(val)
@ -187,14 +192,11 @@ class Document(QWebPage): # {{{
self.set_bottom_padding(0)
self.fit_images()
self.init_hyphenate()
self.initial_left_margin = unicode(self.javascript(
'document.body.style.marginLeft').toString())
self.initial_right_margin = unicode(self.javascript(
'document.body.style.marginRight').toString())
if self.in_paged_mode:
self.switch_to_paged_mode()
self.javascript('full_screen.save_margins()')
if self.in_fullscreen_mode:
self.switch_to_fullscreen_mode()
if self.in_paged_mode:
self.switch_to_paged_mode()
self.read_anchor_positions(use_cache=False)
self.first_load = False
@ -241,7 +243,6 @@ class Document(QWebPage): # {{{
sz.setWidth(scroll_width+side_margin)
self.setPreferredContentsSize(sz)
self.javascript('window.paged_display.fit_images()')
self.javascript('window.paged_display.check_top_margin()')
@property
def column_boundaries(self):
@ -257,27 +258,13 @@ class Document(QWebPage): # {{{
def switch_to_fullscreen_mode(self):
self.in_fullscreen_mode = True
if self.in_paged_mode:
self.javascript('paged_display.max_col_width = %d'%self.max_fs_width)
else:
self.javascript('''
var s = document.body.style;
s.maxWidth = "%dpx";
s.marginLeft = "auto";
s.marginRight = "auto";
'''%self.max_fs_width)
self.javascript('full_screen.on(%d, %s)'%(self.max_fs_width,
'true' if self.in_paged_mode else 'false'))
def switch_to_window_mode(self):
self.in_fullscreen_mode = False
if self.in_paged_mode:
self.javascript('paged_display.max_col_width = %d'%-1)
else:
self.javascript('''
var s = document.body.style;
s.maxWidth = "none";
s.marginLeft = "%s";
s.marginRight = "%s";
'''%(self.initial_left_margin, self.initial_right_margin))
self.javascript('full_screen.off(%s)'%('true' if self.in_paged_mode
else 'false'))
@pyqtSignature("QString")
def debug(self, msg):
@ -463,6 +450,7 @@ class DocumentView(QWebView): # {{{
self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed)
self.connect(self.document, SIGNAL('animated_scroll_done()'),
self.animated_scroll_done, Qt.QueuedConnection)
self.document.page_turn.connect(self.page_turn_requested)
copy_action = self.pageAction(self.document.Copy)
copy_action.setIcon(QIcon(I('convert.png')))
d = self.document
@ -896,6 +884,12 @@ class DocumentView(QWebView): # {{{
self.manager.scrolled(self.scroll_fraction)
#print 'After all:', self.document.ypos
def page_turn_requested(self, backwards):
if backwards:
self.previous_page()
else:
self.next_page()
def scroll_by(self, x=0, y=0, notify=True):
old_pos = (self.document.xpos if self.document.in_paged_mode else
self.document.ypos)

View File

@ -32,10 +32,12 @@ class JavaScriptLoader(object):
'indexing':'ebooks.oeb.display.indexing',
'paged':'ebooks.oeb.display.paged',
'utils':'ebooks.oeb.display.utils',
'fs':'ebooks.oeb.display.full_screen',
}
ORDER = ('jquery', 'jquery_scrollTo', 'bookmarks', 'referencing', 'images',
'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged')
'hyphenation', 'hyphenator', 'utils', 'cfi', 'indexing', 'paged',
'fs')
def __init__(self, dynamic_coffeescript=False):

View File

@ -272,9 +272,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
<h1>%s</h1>
<h3>%s</h3>
<h3>%s</h3>
<h3>%s</h3>
</center>
'''%(_('Full screen mode'),
_('Right click to show controls'),
_('Tap in the left or right page margin to turn pages'),
_('Press Esc to quit')),
self)
self.full_screen_label.setVisible(False)
@ -496,7 +498,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
a.setStartValue(QSize(width, 0))
a.setEndValue(QSize(width, height))
a.start()
QTimer.singleShot(2750, self.full_screen_label.hide)
QTimer.singleShot(3500, self.full_screen_label.hide)
self.view.document.switch_to_fullscreen_mode()
if self.view.document.fullscreen_clock:
self.show_clock()

View File

@ -71,7 +71,6 @@ class Printing(QObject):
paged_display.set_geometry(1, 0, 0, 0);
paged_display.layout();
paged_display.fit_images();
paged_display.check_top_margin();
''')
while True:

View File

@ -352,6 +352,14 @@ class ResultCache(SearchQueryParser): # {{{
'<=':[2, relop_le]
}
local_today = ('_today', icu_lower(_('today')))
local_yesterday = ('_yesterday', icu_lower(_('yesterday')))
local_thismonth = ('_thismonth', icu_lower(_('thismonth')))
local_daysago = icu_lower(_('daysago'))
local_daysago_len = len(local_daysago)
untrans_daysago = '_daysago'
untrans_daysago_len = len('_daysago')
def get_dates_matches(self, location, query, candidates):
matches = set([])
if len(query) < 2:
@ -390,17 +398,24 @@ class ResultCache(SearchQueryParser): # {{{
if relop is None:
(p, relop) = self.date_search_relops['=']
if query == _('today'):
if query in self.local_today:
qd = now()
field_count = 3
elif query == _('yesterday'):
elif query in self.local_yesterday:
qd = now() - timedelta(1)
field_count = 3
elif query == _('thismonth'):
elif query in self.local_thismonth:
qd = now()
field_count = 2
elif query.endswith(_('daysago')):
num = query[0:-len(_('daysago'))]
elif query.endswith(self.local_daysago):
num = query[0:-self.local_daysago_len]
try:
qd = now() - timedelta(int(num))
except:
raise ParseException(query, len(query), 'Number conversion error', self)
field_count = 3
elif query.endswith(self.untrans_daysago):
num = query[0:-self.untrans_daysago_len]
try:
qd = now() - timedelta(int(num))
except:
@ -591,14 +606,23 @@ class ResultCache(SearchQueryParser): # {{{
query = icu_lower(query)
return matchkind, query
local_no = icu_lower(_('no'))
local_yes = icu_lower(_('yes'))
local_unchecked = icu_lower(_('unchecked'))
local_checked = icu_lower(_('checked'))
local_empty = icu_lower(_('empty'))
local_blank = icu_lower(_('blank'))
local_bool_values = (
local_no, local_unchecked, '_no', 'false',
local_yes, local_checked, '_yes', 'true',
local_empty, local_blank, '_empty')
def get_bool_matches(self, location, query, candidates):
bools_are_tristate = self.db_prefs.get('bools_are_tristate')
loc = self.field_metadata[location]['rec_index']
matches = set()
query = icu_lower(query)
if query not in (_('no'), _('unchecked'), '_no', 'false',
_('yes'), _('checked'), '_yes', 'true',
_('empty'), _('blank'), '_empty'):
if query not in self.local_bool_values:
raise ParseException(_('Invalid boolean query "{0}"').format(query))
for id_ in candidates:
item = self._data[id_]
@ -608,20 +632,20 @@ class ResultCache(SearchQueryParser): # {{{
val = force_to_bool(item[loc])
if not bools_are_tristate:
if val is None or not val: # item is None or set to false
if query in [_('no'), _('unchecked'), '_no', 'false']:
if query in (self.local_no, self.local_unchecked, '_no', 'false'):
matches.add(item[0])
else: # item is explicitly set to true
if query in [_('yes'), _('checked'), '_yes', 'true']:
if query in (self.local_yes, self.local_checked, '_yes', 'true'):
matches.add(item[0])
else:
if val is None:
if query in [_('empty'), _('blank'), '_empty', 'false']:
if query in (self.local_empty, self.local_blank, '_empty', 'false'):
matches.add(item[0])
elif not val: # is not None and false
if query in [_('no'), _('unchecked'), '_no', 'true']:
if query in (self.local_no, self.local_unchecked, '_no', 'true'):
matches.add(item[0])
else: # item is not None and true
if query in [_('yes'), _('checked'), '_yes', 'true']:
if query in (self.local_yes, self.local_checked, '_yes', 'true'):
matches.add(item[0])
return matches

View File

@ -17,7 +17,7 @@ from calibre.utils.date import fromtimestamp
from calibre.library.server import listen_on, log_access_file, log_error_file
from calibre.library.server.utils import expose, AuthController
from calibre.utils.mdns import publish as publish_zeroconf, \
stop_server as stop_zeroconf, get_external_ip
unpublish as unpublish_zeroconf, get_external_ip
from calibre.library.server.content import ContentServer
from calibre.library.server.mobile import MobileServer
from calibre.library.server.xml import XMLServer
@ -78,13 +78,18 @@ class BonJour(SimplePlugin): # {{{
SimplePlugin.__init__(self, engine)
self.port = port
self.prefix = prefix
self.mdns_services = [
('Books in calibre', '_stanza._tcp', self.port,
{'path':self.prefix+'/stanza'}),
('Books in calibre', '_calibre._tcp', self.port,
{'path':self.prefix+'/opds'}),
]
def start(self):
try:
publish_zeroconf('Books in calibre', '_stanza._tcp',
self.port, {'path':self.prefix+'/stanza'})
publish_zeroconf('Books in calibre', '_calibre._tcp',
self.port, {'path':self.prefix+'/opds'})
for s in self.mdns_services:
publish_zeroconf(*s)
except:
import traceback
cherrypy.log.error('Failed to start BonJour:')
@ -94,7 +99,8 @@ class BonJour(SimplePlugin): # {{{
def stop(self):
try:
stop_zeroconf()
for s in self.mdns_services:
unpublish_zeroconf(*s)
except:
import traceback
cherrypy.log.error('Failed to stop BonJour:')

File diff suppressed because it is too large Load Diff

View File

@ -871,6 +871,8 @@ class Engine(threading.Thread):
from calibre.constants import DEBUG
try:
rr, wr, er = select.select(rs, [], [], self.timeout)
if globals()['_GLOBAL_DONE']:
continue
for socket in rr:
try:
self.readers[socket].handle_read()
@ -953,11 +955,16 @@ class Reaper(threading.Thread):
return
if globals()['_GLOBAL_DONE']:
return
now = currentTimeMillis()
for record in self.zeroconf.cache.entries():
if record.isExpired(now):
self.zeroconf.updateRecord(now, record)
self.zeroconf.cache.remove(record)
try:
# can get here in a race condition with shutdown. Swallow the
# exception and run around the loop again.
now = currentTimeMillis()
for record in self.zeroconf.cache.entries():
if record.isExpired(now):
self.zeroconf.updateRecord(now, record)
self.zeroconf.cache.remove(record)
except:
pass
class ServiceBrowser(threading.Thread):
@ -1419,6 +1426,9 @@ class Zeroconf(object):
i += 1
nextTime += _UNREGISTER_TIME
def countRegisteredServices(self):
return len(self.services)
def checkService(self, info):
"""Checks the network for a unique service name, modifying the
ServiceInfo passed in if it is not unique."""

View File

@ -240,6 +240,7 @@ class XMLConfig(dict):
def __init__(self, rel_path_to_cf_file):
dict.__init__(self)
self.no_commit = False
self.defaults = {}
self.file_path = os.path.join(config_dir,
*(rel_path_to_cf_file.split('/')))
@ -304,6 +305,7 @@ class XMLConfig(dict):
self.commit()
def commit(self):
if self.no_commit: return
if hasattr(self, 'file_path') and self.file_path:
dpath = os.path.dirname(self.file_path)
if not os.path.exists(dpath):
@ -314,6 +316,13 @@ class XMLConfig(dict):
f.truncate()
f.write(raw)
def __enter__(self):
self.no_commit = True
def __exit__(self, *args):
self.no_commit = False
self.commit()
def to_json(obj):
if isinstance(obj, bytearray):
return {'__class__': 'bytearray',

View File

@ -47,18 +47,8 @@ def start_server():
return _server
def publish(desc, type, port, properties=None, add_hostname=True):
'''
Publish a service.
:param desc: Description of service
:param type: Name and type of service. For example _stanza._tcp
:param port: Port the service listens on
:param properties: An optional dictionary whose keys and values will be put
into the TXT record.
'''
def create_service(desc, type, port, properties, add_hostname):
port = int(port)
server = start_server()
try:
hostname = socket.gethostname().partition('.')[0]
except:
@ -69,13 +59,39 @@ def publish(desc, type, port, properties=None, add_hostname=True):
local_ip = get_external_ip()
type = type+'.local.'
from calibre.utils.Zeroconf import ServiceInfo
service = ServiceInfo(type, desc+'.'+type,
return ServiceInfo(type, desc+'.'+type,
address=socket.inet_aton(local_ip),
port=port,
properties=properties,
server=hostname+'.local.')
def publish(desc, type, port, properties=None, add_hostname=True):
'''
Publish a service.
:param desc: Description of service
:param type: Name and type of service. For example _stanza._tcp
:param port: Port the service listens on
:param properties: An optional dictionary whose keys and values will be put
into the TXT record.
'''
server = start_server()
service = create_service(desc, type, port, properties, add_hostname)
server.registerService(service)
def unpublish(desc, type, port, properties=None, add_hostname=True):
'''
Unpublish a service.
The parameters must be the same as used in the corresponding call to publish
'''
server = start_server()
service = create_service(desc, type, port, properties, add_hostname)
server.unregisterService(service)
if server.countRegisteredServices() == 0:
stop_server()
def stop_server():
global _server
if _server is not None: