merge from trunk

This commit is contained in:
Charles Haley 2013-04-30 19:02:58 +02:00
commit 8e797e0120
118 changed files with 38113 additions and 33570 deletions

View File

@ -20,6 +20,82 @@
# new recipes:
# - title:
- version: 0.9.28
date: 2013-04-26
new features:
- title: "Virtual Libraries: Easily partition your large calibre library into smaller 'virtual' libraries"
type: major
description: "A virtual library is a way to tell calibre to open only a subset of a normal library. For example, you might want to only work with books by a certain author, or books having only a certain tag. To use this feature, click the button labeled 'Virtual Library' to the left of the search bar. For details, see http://manual.calibre-ebook.com/virtual_libraries.html. This feature used to be called 'Search restriction', the new virtual libraries are easier to use, but otherwise fulfil the same function."
- title: "Book details panel: Allow copying of links in the book details panel by right clicking on them."
tickets: [1171963]
- title: "Kobo driver: Add support for the new Kobo Aura HD and firmware version 2.5.0"
tickets: [1169571,1169968]
- title: "Metadata download: When showing downloaded covers, allow right clicking on a cover to view a full size version."
tickets: [1170544]
- title: "Driver for Easy player cyber book e touch and Droid 4"
tickets: [1171633,1170763]
- title: "Edit ToC: Allow the size of the panels in the location view to be adjusted"
- title: "When copying to a library by path, make it more efficient to choose between moving and copying"
tickets: [1168231]
- title: "When checking if a zip/rar file is a comic or contains a single ebook to be auto-extracted, ignore thumbs.db files inside the archive"
bug fixes:
- title: "EPUB Input: Fix handling of EPUB files that contain images with non-ascii filenames."
tickets: [1171186]
- title: "Device driver: Detect Laser EB720 with newer firmware."
tickets: [1171341]
- title: "Fix bug in Danish translation causing books with language Ingush being incorrectly translated as Engelsk"
- title: "PDF Output: Fix hyperlinks not working when converting an EPUB whose individual files have names with URL unsafe characters."
tickets: [1169795]
- title: "Book polishing: Fix inserting cover into an epub with no cover could lead to incorrect guide entry if the opf is not at the root of the epub."
tickets: [1167941]
- title: "ZIP Output: Fix links containing backslashes on windows"
tickets: [1169910]
- title: "Fix polishing of AZW3 files not working on OS X."
tickets: [1168789]
- title: "Polishing books: Fix polishing erroring out if the book being polished has no cover"
- title: "RTF Input: Add partial support for hyperlinks to web resources."
tickets: [1167562]
- title: "Fix book details panel showing incorrect info after deleting books from a connected device"
tickets: [1172839]
improved recipes:
- NZZ Online
- Baltimore Sun
- Metro NL
- Financial Times
- EcoGeek
- comics.com
- Psychology Today
- Science News
new recipes:
- title: Voice of America
author: Krittika Goyal
- title: Lightspeed Magazine
author: Jose Pinto
- title: The Feature
author: Jose Pinto
- version: 0.9.27
date: 2013-04-12

View File

@ -13,13 +13,13 @@ class BaltimoreSun(BasicNewsRecipe):
__author__ = 'Josh Hall'
description = 'Complete local news and blogs from Baltimore'
language = 'en'
version = 2.1
version = 2.5
oldest_article = 1
max_articles_per_feed = 100
use_embedded_content = False
no_stylesheets = True
remove_javascript = True
#auto_cleanup = True
remove_empty_feeds= True
recursions = 1
ignore_duplicate_articles = {'title'}
@ -31,7 +31,7 @@ class BaltimoreSun(BasicNewsRecipe):
match_regexps = [r'page=[0-9]+']
remove_tags = [{'id':["moduleArticleTools","content-bottom","rail","articleRelates module","toolSet","relatedrailcontent","div-wrapper","beta","atp-comments","footer",'gallery-subcontent','subFooter']},
{'class':["clearfix","relatedTitle","articleRelates module","asset-footer","tools","comments","featurePromo","featurePromo fp-topjobs brownBackground","clearfix fullSpan brownBackground","curvedContent",'nextgen-share-tools','outbrainTools', 'google-ad-story-bottom']},
{'class':["clearfix","relatedTitle","articleRelates module","asset-footer","tools","comments","featurePromo","featurePromo fp-topjobs brownBackground","clearfix fullSpan brownBackground","curvedContent",'nextgen-share-tools','nextgen-comments-container','nextgen-comments-content','outbrainTools','fb-like' 'google-ad-story-bottom']},
dict(name='font',attrs={'id':["cr-other-headlines"]})]
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
@ -49,40 +49,39 @@ class BaltimoreSun(BasicNewsRecipe):
'''
feeds = [
## News ##
(u'Top Headlines', u'http://www.baltimoresun.com/rss2.0.xml'),
(u'Breaking News', u'http://www.baltimoresun.com/news/breaking/rss2.0.xml'),
(u'Top Maryland', u'http://www.baltimoresun.com/news/maryland/rss2.0.xml'),
#(u'Anne Arundel County', u'http://www.baltimoresun.com/news/maryland/anne-arundel/rss2.0.xml'),
(u'Baltimore City', u'http://www.baltimoresun.com/news/maryland/baltimore-city/rss2.0.xml'),
#(u'Baltimore County', u'http://www.baltimoresun.com/news/maryland/baltimore-county/rss2.0.xml'),
#(u'Carroll County', u'http://www.baltimoresun.com/news/maryland/carroll/rss2.0.xml'),
#(u'Harford County', u'http://www.baltimoresun.com/news/maryland/harford/rss2.0.xml'),
#(u'Howard County', u'http://www.baltimoresun.com/news/maryland/howard/rss2.0.xml'),
(u'Education', u'http://www.baltimoresun.com/news/education/rss2.0.xml'),
#(u'Obituaries', u'http://www.baltimoresun.com/news/obituaries/rss2.0.xml'),
(u'Local Politics', u'http://www.baltimoresun.com/news/maryland/politics/rss2.0.xml'),
(u'Weather', u'http://www.baltimoresun.com/news/weather/rss2.0.xml'),
#(u'Traffic', u'http://www.baltimoresun.com/features/commuting/rss2.0.xml'),
(u'Top Headlines', u'http://feeds.feedburner.com/baltimoresun/news/rss2'),
(u'Breaking News', u'http://feeds.feedburner.com/baltimoresun/news/local/annearundel/rss2'),
(u'Top Maryland', u'http://feeds.feedburner.com/baltimoresun/news/local/rss2'),
#(u'Anne Arundel County', u'http://feeds.feedburner.com/baltimoresun/news/local/annearundel/rss2'),
(u'Baltimore City', u'http://feeds.feedburner.com/baltimoresun/news/local/baltimore_city/rss20xml'),
#(u'Baltimore County', u'http://feeds.feedburner.com/baltimoresun/news/local/baltimore_county/rss2'),
#(u'Carroll County', u'http://feeds.feedburner.com/baltimoresun/news/local/carroll/rss2'),
#(u'Harford County', u'http://feeds.feedburner.com/baltimoresun/news/local/harford/rss2),
#(u'Howard County', u'http://feeds.feedburner.com/baltimoresun/news/local/howard/rss2'),
(u'Education', u'http://feeds.feedburner.com/baltimoresun/news/education/rss2'),
#(u'Obituaries', u'http://feeds.feedburner.com/baltimoresun/news/obituaries/rss2'),
(u'Local Politics', u'http://feeds.feedburner.com/baltimoresun/news/local/politics/rss2'),
(u'Weather', u'http://feeds.feedburner.com/baltimoresun/news/weather/site/rss2'),
#(u'Traffic', u'http://feeds.feedburner.com/baltimoresun/news/traffic/rss2'),
(u'Nation/world', u'http://feeds.feedburner.com/baltimoresun/news/nationworld/rss2'),
(u'Weird News', u'http://www.baltimoresun.com/news/offbeat/rss2.0.xml'),
#(u'Weird News', u'http://feeds.feedburner.com/baltsun-weirdnews'),
##Sports##
(u'Top Sports', u'http://www.baltimoresun.com/sports/rss2.0.xml'),
(u'Top Sports', u'http://feeds.feedburner.com/baltimoresun/sports/rss2'),
(u'Orioles/Baseball', u'http://www.baltimoresun.com/sports/orioles/rss2.0.xml'),
(u'Ravens/Football', u'http://www.baltimoresun.com/sports/ravens/rss2.0.xml'),
#(u'Terps', u'http://www.baltimoresun.com/sports/terps/rss2.0.xml'),
#(u'College Football', u'http://www.baltimoresun.com/sports/college/football/rss2.0.xml'),
#(u'Lacrosse', u'http://www.baltimoresun.com/sports/college/lacrosse/rss2.0.xml'),
#(u'Horse Racing', u'http://www.baltimoresun.com/sports/horse-racing/rss2.0.xml'),
#(u'Golf', u'http://www.baltimoresun.com/sports/golf/rss2.0.xml'),
#(u'NBA', u'http://www.baltimoresun.com/sports/nba/rss2.0.xml'),
#(u'High School', u'http://www.baltimoresun.com/sports/high-school/rss2.0.xml'),
#(u'Outdoors', u'http://www.baltimoresun.com/sports/outdoors/rss2.0.xml'),
(u'Ravens/Football', u'http://feeds.feedburner.com/baltimoresun/sports/football/rss2'),
#(u'Terps', u''http://feeds.feedburner.com/baltimoresun/sports/terps/rss2'),
#(u'College Football', u''feed://feeds.feedburner.com/baltimoresun/sports/college/football/rss2'),
#(u'Lacrosse', u'http://feeds.feedburner.com/baltimoresun/sports/college/lacrosse/rss2'),
#(u'Horse Racing', u'http://feeds.feedburner.com/baltimoresun/sports/horseracing/rss2'),
#(u'Golf', u'http://feeds.feedburner.com/baltimoresun/sports/golf/rss2'),
#(u'NBA', u'http://feeds.feedburner.com/baltimoresun/sports/basketball/rss2'),
#(u'High School', u'http://feeds.feedburner.com/baltimoresun/sports/highschool/rss2'),
#(u'Outdoors', u'http://feeds.feedburner.com/baltimoresun/sports/outdoors/rss2'),
## Entertainment ##
(u'Celebrity News', u'http://www.baltimoresun.com/entertainment/celebrities/rss2.0.xml'),
(u'Arts & Theater', u'http://www.baltimoresun.com/entertainment/arts/rss2.0.xml'),
(u'Celebrity News', u'http://baltimore.feedsportal.com/c/34255/f/623042/index.rss'),
(u'Arts & Theater', u'http://feeds.feedburner.com/baltimoresun/entertainment/galleriesmuseums/rss2'),
(u'Movies', u'http://www.baltimoresun.com/entertainment/movies/rss2.0.xml'),
(u'Music & Nightlife', u'http://www.baltimoresun.com/entertainment/music/rss2.0.xml'),
(u'Restaurants & Food', u'http://www.baltimoresun.com/entertainment/dining/rss2.0.xml'),
@ -92,7 +91,6 @@ class BaltimoreSun(BasicNewsRecipe):
(u'Health&Wellness', u'http://www.baltimoresun.com/health/rss2.0.xml'),
(u'Home & Garden', u'http://www.baltimoresun.com/features/home-garden/rss2.0.xml'),
(u'Living Green', u'http://www.baltimoresun.com/features/green/rss2.0.xml'),
(u'Parenting', u'http://www.baltimoresun.com/features/parenting/rss2.0.xml'),
(u'Fashion', u'http://www.baltimoresun.com/features/fashion/rss2.0.xml'),
(u'Travel', u'http://www.baltimoresun.com/travel/rss2.0.xml'),
#(u'Faith', u'http://www.baltimoresun.com/features/faith/rss2.0.xml'),
@ -100,17 +98,17 @@ class BaltimoreSun(BasicNewsRecipe):
## Business ##
(u'Top Business', u'http://www.baltimoresun.com/business/rss2.0.xml'),
(u'Technology', u'http://www.baltimoresun.com/business/technology/rss2.0.xml'),
(u'Personal finance', u'http://www.baltimoresun.com/business/money/rss2.0.xml'),
(u'Personal finance', u'http://baltimore.feedsportal.com/c/34255/f/623057/index.rss'),
(u'Real Estate', u'http://www.baltimoresun.com/classified/realestate/rss2.0.xml'),
(u'Jobs', u'http://www.baltimoresun.com/classified/jobs/rss2.0.xml'),
(u'DIY', u'http://www.baltimoresun.com/features/do-it-yourself/rss2.0.xml'),
(u'Consumer Safety', u'http://www.baltimoresun.com/business/consumer-safety/rss2.0.xml'),
(u'Jobs', u'http://baltimore.feedsportal.com/c/34255/f/623059/index.rss'),
#(u'DIY', u'http://baltimore.feedsportal.com/c/34255/f/623060/index.rss'),
#(u'Consumer Safety', u'http://baltimore.feedsportal.com/c/34255/f/623061/index.rss'),
(u'Investing', u'http://www.baltimoresun.com/business/money/rss2.0.xml'),
## Opinion##
(u'Sun Editorials', u'http://www.baltimoresun.com/news/opinion/editorial/rss2.0.xml'),
(u'Op/Ed', u'http://www.baltimoresun.com/news/opinion/oped/rss2.0.xml'),
(u'Readers Respond', u'http://www.baltimoresun.com/news/opinion/readersrespond/'),
(u'Readers Respond', u'http://baltimore.feedsportal.com/c/34255/f/623065/index.rss'),
## Columnists ##
(u'Kevin Cowherd', u'http://www.baltimoresun.com/sports/bal-columnist-cowherd,0,6829726.columnist-rss2.0.xml'),
@ -138,30 +136,26 @@ class BaltimoreSun(BasicNewsRecipe):
(u'The Real Estate Wonk', u'http://www.baltimoresun.com/business/real-estate/wonk/rss2.0.xml'),
## Entertainment Blogs ##
(u'Clef Notes & Drama Queens', 'http://weblogs.baltimoresun.com/entertainment/classicalmusic/index.xml'),
(u'ArtSmash', 'http://www.baltimoresun.com/entertainment/arts/artsmash/rss2.0.xml'),
(u'Baltimore Diner', u'http://baltimore.feedsportal.com/c/34255/f/623088/index.rss'),
(u'Midnight Sun', u'http://www.baltimoresun.com/entertainment/music/midnight-sun-blog/rss2.0.xml'),
(u'Read Street', u'http://www.baltimoresun.com/features/books/read-street/rss2.0.xml'),
(u'Z on TV', u'http://www.baltimoresun.com/entertainment/tv/z-on-tv-blog/rss2.0.xml'),
### Life Blogs ##
## Life Blogs ##
#(u'BMore Green', u'http://weblogs.baltimoresun.com/features/green/index.xml'),
#(u'Baltimore Insider',u'http://www.baltimoresun.com/features/baltimore-insider-blog/rss2.0.xml'),
#(u'Homefront', u'http://www.baltimoresun.com/features/parenting/homefront/rss2.0.xml'),
#(u'Picture of Health', u'http://www.baltimoresun.com/health/blog/rss2.0.xml'),
(u'Baltimore Insider',u'http://www.baltimoresun.com/features/baltimore-insider-blog/rss2.0.xml'),
(u'Picture of Health', u'http://www.baltimoresun.com/health/blog/rss2.0.xml'),
#(u'Unleashed', u'http://weblogs.baltimoresun.com/features/mutts/blog/index.xml'),
## b the site blogs ##
(u'Game Cache', u'http://www.baltimoresun.com/entertainment/bthesite/game-cache/rss2.0.xml'),
(u'TV Lust', u'http://www.baltimoresun.com/entertainment/bthesite/tv-lust/rss2.0.xml'),
(u'TV Lust', u'http://baltimore.feedsportal.com/c/34255/f/623096/index.rss'),
## Sports Blogs ##
(u'Baltimore Sports Blitz', u'http://baltimore.feedsportal.com/c/34255/f/623097/index.rss'),
#(u'Faceoff', u'http://weblogs.baltimoresun.com/sports/lacrosse/blog/index.xml'),
#(u'MMA Stomping Grounds', u'http://weblogs.baltimoresun.com/sports/mma/blog/index.xml'),
## (u'Lacrosse Insider',u'http://www.baltimoresun.com/sports/lacrosse-blog/rss2.0.xml'),
(u'Orioles Insider', u'http://baltimore.feedsportal.com/c/34255/f/623100/index.rss'),
(u'Ravens Insider', u'http://www.baltimoresun.com/sports/ravens/ravens-insider/rss2.0.xml'),
#(u'Recruiting Report', u'http://weblogs.baltimoresun.com/sports/college/recruiting/index.xml'),
#(u'Ring Posts', u'http://weblogs.baltimoresun.com/sports/wrestling/blog/index.xml'),
(u'The Schmuck Stops Here', u'http://www.baltimoresun.com/sports/schmuck-blog/rss2.0.xml'),
#(u'Tracking the Terps', u'http://weblogs.baltimoresun.com/sports/college/maryland_terps/blog/index.xml'),
@ -169,7 +163,6 @@ class BaltimoreSun(BasicNewsRecipe):
]
def get_article_url(self, article):
ans = None
try:
@ -190,6 +183,8 @@ class BaltimoreSun(BasicNewsRecipe):
url = a.get('href')
if url:
return self.index_to_soup(url, raw=True)
def print_version(self, url):
return self.browser.open_novisit(url).geturl()
def postprocess_html(self, soup, first_fetch):
# Remove the navigation bar. It was kept until now to be able to follow

View File

@ -12,7 +12,7 @@ class BusinessWeekMagazine(BasicNewsRecipe):
category = 'news'
encoding = 'UTF-8'
keep_only_tags = [
dict(name='div', attrs={'id':'article_body_container'}),
dict(name='div', attrs={'id':['article_body_container','story_body']}),
]
remove_tags = [dict(name='ui'),dict(name='li'),dict(name='div', attrs={'id':['share-email']})]
no_javascript = True
@ -26,43 +26,45 @@ class BusinessWeekMagazine(BasicNewsRecipe):
#Find date
mag=soup.find('h2',text='Magazine')
self.log(mag)
dates=self.tag_to_string(mag.findNext('h3'))
self.timefmt = u' [%s]'%dates
#Go to the main body
div0 = soup.find ('div', attrs={'class':'column left'})
div0 = soup.find('div', attrs={'class':'column left'})
section_title = ''
feeds = OrderedDict()
for div in div0.findAll(['h4','h5']):
for div in div0.findAll('a', attrs={'class': None}):
articles = []
section_title = self.tag_to_string(div.findPrevious('h3')).strip()
title=self.tag_to_string(div.a).strip()
url=div.a['href']
title=self.tag_to_string(div).strip()
url=div['href']
soup0 = self.index_to_soup(url)
urlprint=soup0.find('a', attrs={'href':re.compile('.*printer.*')})['href']
articles.append({'title':title, 'url':urlprint, 'description':'', 'date':''})
urlprint=soup0.find('a', attrs={'href':re.compile('.*printer.*')})
if urlprint is not None:
url=urlprint['href']
articles.append({'title':title, 'url':url, 'description':'', 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
div1 = soup.find ('div', attrs={'class':'column center'})
div1 = soup.find('div', attrs={'class':'column center'})
section_title = ''
for div in div1.findAll(['h4','h5']):
for div in div1.findAll('a'):
articles = []
desc=self.tag_to_string(div.findNext('p')).strip()
section_title = self.tag_to_string(div.findPrevious('h3')).strip()
title=self.tag_to_string(div.a).strip()
url=div.a['href']
title=self.tag_to_string(div).strip()
url=div['href']
soup0 = self.index_to_soup(url)
urlprint=soup0.find('a', attrs={'href':re.compile('.*printer.*')})['href']
articles.append({'title':title, 'url':urlprint, 'description':desc, 'date':''})
urlprint=soup0.find('a', attrs={'href':re.compile('.*printer.*')})
if urlprint is not None:
url=urlprint['href']
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

View File

@ -16,7 +16,8 @@ class i09(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = True
use_embedded_content = False
auto_cleanup = True
language = 'en'
masthead_url = 'http://cache.gawkerassets.com/assets/io9.com/img/logo.png'
extra_css = '''
@ -33,10 +34,6 @@ class i09(BasicNewsRecipe):
feeds = [(u'Articles', u'http://feeds.gawker.com/io9/vip?format=xml')]
remove_tags = [
{'class': 'feedflare'},
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -42,7 +42,6 @@ class Nzz(BasicNewsRecipe):
soup = self.index_to_soup(baseref)
articles = {}
key = None
ans = []
issuelist = soup.find(id="issueSelectorList")
@ -52,7 +51,6 @@ class Nzz(BasicNewsRecipe):
section = f.string
sectionref = baseref + f['href']
# print "section is "+section +" and ref is "+sectionref
ans.append(section)
articlesoup = self.index_to_soup(sectionref)
@ -68,7 +66,6 @@ class Nzz(BasicNewsRecipe):
pubdate = strftime('%a, %d %b')
if not artcaption is None:
# print " found article named "+artcaption+" at "+arthref
if not articles.has_key(section):
articles[section] = []
articles[section].append(
@ -80,10 +77,10 @@ class Nzz(BasicNewsRecipe):
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
br.open('https://webpaper.nzz.ch/login')
br.open('https://cas.nzz.ch/cas/login')
br.select_form(nr=0)
br['_username'] = self.username
br['_password'] = self.password
br['username'] = self.username
br['password'] = self.password
br.submit()
return br

View File

@ -20,14 +20,13 @@ class AdvancedUserRecipe1279258912(BasicNewsRecipe):
description = 'Orlando, Florida, Newspaper'
category = 'News, Orlando, Florida'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'utf-8'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://www.orlandosentinel.com/media/graphic/2009-07/46844851.gif'
remove_empty_feeds = True
auto_cleanup = True
@ -45,7 +44,7 @@ class AdvancedUserRecipe1279258912(BasicNewsRecipe):
link=link.split('/')[-2]
encoding = {'0B': '.', '0C': '/', '0A': '0', '0F': '=', '0G': '&',
'0D': '?', '0E': '-', '0N': '.com', '0L': 'http:',
'0S':'//'}
'0S':'//', '0H':','}
for k, v in encoding.iteritems():
link = link.replace(k, v)
ans = link

View File

@ -1,6 +1,15 @@
"""
Pocket Calibre Recipe v1.2
Pocket Calibre Recipe v1.3
"""
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
import urllib2
import urllib
import json
import operator
import tempfile
import re
__license__ = 'GPL v3'
__copyright__ = '''
2010, Darko Miletic <darko.miletic at gmail.com>
@ -8,9 +17,6 @@ __copyright__ = '''
2012, tBunnyMan <Wag That Tail At Me dot com>
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
class Pocket(BasicNewsRecipe):
title = 'Pocket'
@ -21,109 +27,150 @@ class Pocket(BasicNewsRecipe):
read after downloading.'''
publisher = 'getpocket.com'
category = 'news, custom'
oldest_article = 7
max_articles_per_feed = 50
minimum_articles = 10
mark_as_read_after_dl = True
#Set this to False for testing
mark_as_read_after_dl = False
#MUST be either 'oldest' or 'newest'
sort_method = 'oldest'
#To filter by tag this needs to be a single tag in quotes; IE 'calibre'
only_pull_tag = None
#You don't want to change anything under here unless you REALLY know what you are doing
no_stylesheets = True
use_embedded_content = False
needs_subscription = True
INDEX = u'http://getpocket.com'
LOGIN = INDEX + u'/l'
readList = []
articles_are_obfuscated = True
apikey = '19eg0e47pbT32z4793Tf021k99Afl889'
index_url = u'http://getpocket.com'
ajax_url = u'http://getpocket.com/a/x/getArticle.php'
read_api_url = index_url + u'/v3/get'
modify_api_url = index_url + u'/v3/send'
legacy_login_url = index_url + u'/l' # We use this to cheat oAuth
articles = []
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
if self.username is not None:
br.open(self.LOGIN)
def get_browser(self, *args, **kwargs):
"""
We need to pretend to be a recent version of safari for the mac to prevent User-Agent checks
Pocket api requires username and password so fail loudly if it's missing from the config.
"""
br = BasicNewsRecipe.get_browser(self,
user_agent='Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-us) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4')
if self.username is not None and self.password is not None:
br.open(self.legacy_login_url)
br.select_form(nr=0)
br['feed_id'] = self.username
if self.password is not None:
br['password'] = self.password
br.submit()
else:
self.user_error("This Recipe requires authentication, please configured user & pass")
return br
def get_feeds(self):
self.report_progress(0, ('Fetching list of pages...'))
lfeeds = []
i = 1
feedurl = self.INDEX + u'/unread/1'
while True:
title = u'Unread articles, page ' + str(i)
lfeeds.insert(0, (title, feedurl))
self.report_progress(0, ('Got ') + str(i) + (' pages'))
i += 1
soup = self.index_to_soup(feedurl)
ritem = soup.find('a', attrs={'id':'next', 'class':'active'})
if ritem is None:
break
feedurl = self.INDEX + ritem['href']
return lfeeds
def get_auth_uri(self):
"""Quick function to return the authentication part of the url"""
uri = ""
uri = u'{0}&apikey={1!s}'.format(uri, self.apikey)
if self.username is None or self.password is None:
self.user_error("Username or password is blank. Pocket no longer supports blank passwords")
else:
uri = u'{0}&username={1!s}'.format(uri, self.username)
uri = u'{0}&password={1!s}'.format(uri, self.password)
return uri
def get_pull_articles_uri(self):
"""Return the part of the uri that has all of the get request settings"""
uri = ""
uri = u'{0}&state={1}'.format(uri, u'unread') # TODO This could be modded to allow pulling archives
uri = u'{0}&contentType={1}'.format(uri, u'article') # TODO This COULD return images too
uri = u'{0}&sort={1}'.format(uri, self.sort_method)
uri = u'{0}&count={1!s}'.format(uri, self.max_articles_per_feed)
if self.only_pull_tag is not None:
uri = u'{0}tag={1}'.format(uri, self.only_pull_tag)
return uri
def parse_index(self):
totalfeeds = []
articlesToGrab = self.max_articles_per_feed
lfeeds = self.get_feeds()
for feedobj in lfeeds:
if articlesToGrab < 1:
break
feedtitle, feedurl = feedobj
self.report_progress(0, ('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
soup = self.index_to_soup(feedurl)
ritem = soup.find('ul', attrs={'id':'list'})
if ritem is None:
self.log.exception("Page %s skipped: invalid HTML" % (feedtitle if feedtitle else feedurl))
continue
for item in reversed(ritem.findAll('li')):
if articlesToGrab < 1:
break
else:
articlesToGrab -= 1
description = ''
atag = item.find('a', attrs={'class':'text'})
if atag and atag.has_key('href'):
url = self.INDEX + atag['href']
title = self.tag_to_string(item.div)
date = strftime(self.timefmt)
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
readLink = item.find('a', attrs={'class':'check'})['href']
self.readList.append(readLink)
totalfeeds.append((feedtitle, articles))
if len(self.readList) < self.minimum_articles:
self.mark_as_read_after_dl = False
if hasattr(self, 'abort_recipe_processing'):
self.abort_recipe_processing("Only %d articles retrieved, minimum_articles not reached" % len(self.readList))
else:
self.log.exception("Only %d articles retrieved, minimum_articles not reached" % len(self.readList))
pocket_feed = []
fetch_url = u"{0}?{1}{2}".format(
self.read_api_url,
self.get_auth_uri(),
self.get_pull_articles_uri()
)
try:
request = urllib2.Request(fetch_url)
response = urllib2.urlopen(request)
pocket_feed = json.load(response)['list']
except urllib2.HTTPError as e:
self.log.exception("Pocket returned an error: {0}\nurl: {1}".format(e, fetch_url))
return []
return totalfeeds
except urllib2.URLError as e:
self.log.exception("Unable to connect to getpocket.com's api: {0}\nurl: {1}".format(e, fetch_url))
return []
if len(pocket_feed) < self.minimum_articles:
self.mark_as_read_after_dl = False
self.user_error("Only {0} articles retrieved, minimum_articles not reached".format(len(pocket_feed)))
def mark_as_read(self, markList):
br = self.get_browser()
for link in markList:
url = self.INDEX + link
print 'Marking read: ', url
response = br.open(url)
print response.info()
for pocket_article in pocket_feed.iteritems():
self.articles.append({
'item_id': pocket_article[0],
'title': pocket_article[1]['resolved_title'],
'date': pocket_article[1]['time_updated'],
'url': u'{0}/a/read/{1}'.format(self.index_url, pocket_article[0]),
'real_url': pocket_article[1]['resolved_url'],
'description': pocket_article[1]['excerpt'],
'sort': pocket_article[1]['sort_id']
})
self.articles = sorted(self.articles, key=operator.itemgetter('sort'))
print self.articles
return [("My Pocket Articles for {0}".format(strftime('[%I:%M %p]')), self.articles)]
def get_obfuscated_article(self, url):
soup = self.index_to_soup(url)
formcheck_script_tag = soup.find('script', text=re.compile("formCheck"))
form_check = formcheck_script_tag.split("=")[1].replace("'", "").replace(";", "").strip()
article_id = url.split("/")[-1]
data = urllib.urlencode({'itemId': article_id, 'formCheck': form_check})
response = self.browser.open(self.ajax_url, data)
article_json = json.load(response)['article']['article']
with tempfile.NamedTemporaryFile(delete=False) as tf:
tf.write(article_json)
return tf.name
def mark_as_read(self, mark_list):
formatted_list = []
for article_id in mark_list:
formatted_list.append({
'action': 'archive',
'item_id': article_id
})
command = {
'actions': formatted_list
}
mark_read_url = u'{0}?{1}'.format(
self.modify_api_url,
self.get_auth_uri()
)
try:
request = urllib2.Request(mark_read_url, json.dumps(command))
response = urllib2.urlopen(request)
print u'response = {0}'.format(response.info())
except urllib2.HTTPError as e:
self.log.exception('Pocket returned an error while archiving articles: {0}'.format(e))
return []
except urllib2.URLError as e:
self.log.exception("Unable to connect to getpocket.com's modify api: {0}".format(e))
return []
def cleanup(self):
if self.mark_as_read_after_dl:
self.mark_as_read(self.readList)
self.mark_as_read([x[1]['item_id'] for x in self.articles])
else:
pass
def default_cover(self, cover_file):
'''
"""
Create a generic cover for recipes that don't have a cover
This override adds time to the cover
'''
"""
try:
from calibre.ebooks import calibre_cover
title = self.title if isinstance(self.title, unicode) else \
@ -137,3 +184,12 @@ class Pocket(BasicNewsRecipe):
self.log.exception('Failed to generate default cover')
return False
return True
def user_error(self, error_message):
if hasattr(self, 'abort_recipe_processing'):
self.abort_recipe_processing(error_message)
else:
self.log.exception(error_message)
raise RuntimeError(error_message)
# vim:ft=python

View File

@ -25,7 +25,7 @@ class Smithsonian(BasicNewsRecipe):
soup = self.index_to_soup(current_issue_url)
#Go to the main body
div = soup.find ('div', attrs={'id':'article-body'})
div = soup.find('div', attrs={'id':'article-body'})
#Find date
date = re.sub('.*\:\W*', "", self.tag_to_string(div.find('h2')).strip())
@ -49,16 +49,20 @@ class Smithsonian(BasicNewsRecipe):
self.log('Found section:', section_title)
else:
link=post.find('a',href=True)
article_cat=link.findPrevious('p', attrs={'class':'article-cat'})
url=link['href']+'?c=y&story=fullstory'
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=self.tag_to_string(link).strip()+ u' (%s)'%author
description=self.tag_to_string(post.findAll('p')[-1]).strip()
title=self.tag_to_string(link).strip()
if article_cat is not None:
title += u' (%s)'%self.tag_to_string(article_cat).strip()
self.log('\tFound article:', title)
articles.append({'title':title, 'url':url, 'description':desc, 'date':''})
articles.append({'title':title, 'url':url, 'description':description, 'date':''})
if articles:
feeds[section_title] = articles
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
articles = []
ans = [(key, val) for key, val in feeds.iteritems()]
return ans

View File

@ -22,3 +22,6 @@ class XkcdCom(BasicNewsRecipe):
]
extra_css = "#photo_text{font-size:small;}"
feeds = [(u'What If...', u'http://what-if.xkcd.com/feed.atom')]

View File

@ -0,0 +1,352 @@
Notes on building libiMobileDevice for Windows
=========================================================
1. Get source files, set up VS project
2. Build libcnary
3. Build libgen
4. Build libplist
5. Build libusbmuxd
6. Build libimobiledevice
7. Exporting libimobiledevice entry points
8. Finished
Get source files, set up VS project
-------------------------------------
Starting with source downloaded from https://github.com/storoj/libimobiledevice-win32
Now create a new directory in which we will work::
mkdir imobiledevice
cp -r libcnary libgen vendors/include libimobiledevice libplist libusbmuxd imobiledevice
cd imobiledevice
rm `find . -name '*.props'`
rm `find . -name *.vcxproj*`
rm `find . -name *.txt`
cd ..
mv imobiledevice ~/sw/private/
In include/unistd.h, comment out line 11::
// #include <getopt.h> /* getopt from: http://www.pwilson.net/sample.html.
Create a new VS 2008 Project
- File|New|Project…
- Visual C++: Win32
- Template: Win32Project
- Name: imobiledevice
- Location: Choose ~/sw/private
- Solution: (Uncheck the create directory for solution checkbox)
- Click OK
- Next screen, select Application Settings tab
- Application type: DLL
- Additional options: Empty project
- Click Finish
In the tool bar Solution Configurations dropdown, select Release.
In the tool bar Solution Platforms dropdown, select Win32.
(For 64 bit choose new configuration and create x64 with properties copied from
win32).
Build libcnary
-------------------------
In VS Solution Explorer, right-click Solution 'imobiledevice', then click
Add|New Project.
- Name: libcnary
- Location: Add \imobiledevice to the end of the default location
- Visual C++: Win32, Template: Win32 Project
- Click OK
- Application Settings: Static library (not using precompiled headers)
- Click Finish
In VS Solution Explorer, select the libcnary project, Project->Show All files.
- Right-click the include folder, select 'Include In Project'.
- Select all the .c files, right click, select 'Include In Project'
- Select all the .c files, right click -> Properties -> C/C++ -> Advanced -> Compile as C++ code
- Properties|Configuration Properties|C/C++:
General|Additional Include Directories:
"$(ProjectDir)\include"
- If 64bits, then Right click->Properties->Configuration Manager change
Win32 to x64 for the libcnary project and check the Build checkbox
- Right-click libcnary, Build. Should build with 0 errors, 0 warnings.
Build libplist
---------------------
In VS Solution Explorer, right-click Solution 'imobiledevice', then click
Add|New Project.
- Name: libplist
- Visual C++: Win32, Template: Win32 Project
- Location: Add \imobiledevice to the end of the default location
- Click OK
- Application Settings: DLL (Empty project)
- Click Finish
In VS Solution Explorer, select the libplist project, then click the 'Show all files'
button.
- Right-click the include folder, select Include In Project
- Right-click the src folder, select Include In Project
- Set 7 C files to compile as C++
Advanced|Compile As: Compile as C++ Code (/TP)
base64.c, bplist.c, bytearray.c, hashtable.c, plist.c, ptarray.c, xplist.c
- Properties|Configuration Properties|C/C++:
General|Additional Include Directories:
$(ProjectDir)\include
$(SolutionDir)\include
$(SolutionDir)\libcnary\include
$SW\include\libxml2 (if it exists)
$SW\include (make sure this is last in the list)
- Properties|C/C++|Preprocessor
Preprocessor Definitions: Add the following items
__STDC_FORMAT_MACROS
plist_EXPORTS
- Properties -> Linker -> General -> Additional Library directories: ~/sw/lib (for libxml2.lib)
- Properties -> Linker -> Input -> Additional Dependencies: libxml2.lib
- Project Dependencies:
Depends on: libcnary
- If 64bits, then Right click->Properties->Configuration Manager change
Win32 to x64 for the libcnary project and check the Build checkbox
- Right-click libplist, Build. Should build with 0 errors (there will be
warnings about datatype conversion for the 64 bit build)
Build libusbmuxd
----------------------
In VS Solution Explorer, right-click Solution 'imobiledevice', then click
Add|New Project.
- Name: libusbmuxd
- Visual C++: Win32, Template: Win32 Project
- Location: Add \imobiledevice to the end of the default location
- Click OK
- Application Settings: DLL (Empty project)
- Click Finish
In VS Solution Explorer, select the libusbmuxd project, then click the 'Show all files'
button.
- Select all 7 files, right-click, Include In Project.
- Set 3 C files to compile as C++
Advanced|Compile As: Compile as C++ Code (/TP)
libusbmuxd.c, sock_stuff.c, utils.c
- Properties|Configuration Properties|C/C++:
General|Additional Include Directories:
$(SolutionDir)\include
$(SolutionDir)\libplist\include
- Properties|Linker|Input|Additional Dependencies:
ws2_32.lib
- Properties|C/C++|Preprocessor
Preprocessor Definitions: add 'HAVE_PLIST'
- Project Dependencies:
Depends on: libplist
- Edit sock_stuff.c #227:
fprintf(stderr, "%s: gethostbyname returned NULL address!\n",
__FUNCTION__);
- Edit libusbmuxd\usbmuxd.h, insert at #26:
#ifdef LIBUSBMUXD_EXPORTS
# define LIBUSBMUXD_API __declspec( dllexport )
#else
# define LIBUSBMUXD_API __declspec( dllimport )
#endif
Then, at each function, insert LIBUSBMUXD_API ahead of declaration:
usbmuxd_subscribe
usbmuxd_unsubscribe
usbmuxd_get_device_list
usbmuxd_device_list_free
usbmuxd_get_device_by_udid
usbmuxd_connect
usbmuxd_disconnect
usbmuxd_send
usbmuxd_recv_timeout
usbmuxd_recv
usbmuxd_set_use_inotify
usbmuxd_set_debug_level
- If 64bits, then Right click->Properties->Configuration Manager change
Win32 to x64 for the libcnary project and check the Build checkbox
- Right-click libusbmuxd, Build. Should build with 0 errors, 10 or 14 warnings
Build libgen
-----------------------
In VS Solution Explorer, right-click Solution 'imobiledevice', then click
Add|New Project.
- Name: libgen
- Visual C++: Win32, Template: Win32 Project
- Location: Add \imobiledevice to the end of the default location
- Click OK
- Application Settings: Static library (not using precompiled headers)
- Click Finish
In VS Solution Explorer, select the libgen project, then click the 'Show all files'
button.
- Select libgen.cpp and libgen.h, right click, select 'Include In Project'
- Open libgen.cpp, comment out line 5::
// #include <fileapi.h>
(This is a Windows 8 include file, not needed to build in Win 7)
- If 64bits, then Right click->Properties->Configuration Manager change
Win32 to x64 for the libcnary project and check the Build checkbox
- Right-click libgen, Build. Should build with 0 errors, 0 warnings.
Build libimobiledevice
----------------------------
In VS Solution Explorer, right-click Solution 'imobiledevice', then click
Add|New Project.
- Name: libimobiledevice
- Visual C++: Win32, Template: Win32 Project
- Location: Add \imobiledevice to the end of the default location
- Click OK
- Application Settings: DLL (Empty project)
- Click Finish
- Right-click the include folder, select Include In Project
- Right-click the src folder, select Include In Project
- Set .c files to compile as C++
Advanced|Compile As: Compile as C++ Code (/TP)
- Properties|Configuration Properties|C/C++:
General|Additional Include Directories:
$(ProjectDir)\include
$(SolutionDir)\include
$(SolutionDir)\libplist\include
$(SolutionDir)\libgen
$(SolutionDir)\libusbmuxd
$SW\private\openssl\include
- Properties -> Linker -> General -> Additional library directories:
$SW\private\openssl\lib
$(OutDir)
- Properties|Linker|Input|Additional Dependencies:
libeay32.lib
ssleay32.lib
libplist.lib
libgen.lib
libusbmuxd.lib
ws2_32.lib
- Properties|C/C++|Preprocessor
Preprocessor Definitions:
ASN1_STATIC
HAVE_OPENSSL
__LITTLE_ENDIAN__
_LIB
- Project Dependencies:
libcnary
libgen
libplist
libusbmuxd
- Edit afc.c #35:
Comment out lines 35-37 (Synchapi.h is a Windows 8 include file)
- Edit userprofile.c and add at line 25:
#include <Windows.h>
- Edit libimobiledevice\include\libimobiledevice\afc.h
At #26, insert
#define AFC_API __declspec( dllexport )
Then, at each function, insert AFC_API ahead of declaration
afc_client_new
afc_client_free
afc_get_device_info
afc_read_directory
afc_get_file_info
afc_file_open
afc_file_close
afc_file_lock
afc_file_read
afc_file_write
afc_file_seek
afc_file_tell
afc_file_truncate
afc_remove_path
afc_rename_path
afc_make_directory
afc_truncate
afc_make_link
afc_set_file_time
afc_get_device_info_key
- Edit libimobiledevice\include\libimobiledevice\housearrest.h
At #26, insert
#define HOUSE_ARREST_API __declspec( dllexport )
Then, at each function, insert HOUSE_ARREST_API ahead of declaration
house_arrest_client_new
house_arrest_client_free
house_arrest_send_request
house_arrest_send_command
house_arrest_get_result
afc_client_new_from_house_arrest_client
- Edit libimobiledevice\include\libimobiledevice\installation_proxy.h
At #26, insert
#define INSTALLATION_PROXY_API __declspec( dllexport )
Then, at each function, insert INSTALLATION_PROXY_API ahead of declaration
instproxy_client_new
instproxy_client_free
instproxy_browse
instproxy_install
instproxy_upgrade
instproxy_uninstall
instproxy_lookup_archives
instproxy_archive
instproxy_restore
instproxy_remove_archive
instproxy_client_options_new
instproxy_client_options_add
instproxy_client_options_free
- Edit libimobiledevice\include\libimobiledevice\libimobiledevice.h
At #26, insert
#define LIBIMOBILEDEVICE_API __declspec( dllexport )
Then, at each function, insert LIBIMOBILEDEVICE_API ahead of declaration
idevice_set_debug_level
idevice_event_subscribe
idevice_event_unsubscribe
idevice_get_device_list
idevice_device_list_free
idevice_new
idevice_free
idevice_connect
idevice_disconnect
idevice_connection_send
idevice_connection_receive_timeout
idevice_connection_receive
idevice_get_handle
idevice_get_udid
- Edit libimobiledevice\include\libimobiledevice\lockdown.h
At #27, insert
#define LOCKDOWN_API __declspec( dllexport )
Then, at each function, insert LOCKDOWN_API ahead of declaration
lockdownd_client_new
lockdownd_client_new_with_handshake
lockdownd_client_free
lockdownd_query_type
lockdownd_get_value
lockdownd_set_value
lockdownd_remove_value
lockdownd_start_service
lockdownd_start_session
lockdownd_stop_session
lockdownd_send
lockdownd_receive
lockdownd_pair
lockdownd_validate_pair
lockdownd_unpair
lockdownd_activate
lockdownd_deactivate
lockdownd_enter_recovery
lockdownd_goodbye
lockdownd_getdevice_udid
lockdownd_get_device_name
lockdownd_get_sync_data
lockdownd_data_classes_free
lockdownd_service_descriptor_free
- If 64bits, then Right click->Properties->Configuration Manager change
Win32 to x64 for the libcnary project and check the Build checkbox
- Right-click libimobiledevice, Build.
0 errors, 60 warnings.
Copy the DLLs
-----------------
Run::
cp `find . -name '*.dll'` ~/sw/bin/

View File

@ -540,6 +540,11 @@ Then open ChmLib.dsw in Visual Studio, change the configuration to Release
(Win32|x64) and build solution, this will generate a static library in
Release/ChmLib.lib
libimobiledevice
------------------
See libimobiledevice_notes.rst
calibre
---------

View File

@ -10,15 +10,18 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2011-08-27 05:57+0000\n"
"Last-Translator: Mohammad Gamal <f2c2001@yahoo.com>\n"
"Language-Team: Arabic <support@arabeyes.org>\n"
"PO-Revision-Date: 2013-04-15 10:56+0000\n"
"Last-Translator: LADHARI <nader.ladhari@gmail.com>\n"
"Language-Team: awadh alghaamdi <awadh_al_ghaamdi@hotmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-11-26 05:06+0000\n"
"X-Generator: Launchpad (build 14381)\n"
"X-Launchpad-Export-Date: 2013-04-16 04:37+0000\n"
"X-Generator: Launchpad (build 16564)\n"
"X-Poedit-Country: SAUDI ARABIA\n"
"Language: ar\n"
"X-Poedit-Language: Arabic\n"
"X-Poedit-SourceCharset: utf-8\n"
#. name for aaa
msgid "Ghotuo"
@ -66,7 +69,7 @@ msgstr ""
#. name for aam
msgid "Aramanik"
msgstr ""
msgstr "ارامانيك"
#. name for aan
msgid "Anambé"
@ -110,7 +113,7 @@ msgstr ""
#. name for aaz
msgid "Amarasi"
msgstr ""
msgstr "أماراسي"
#. name for aba
msgid "Abé"
@ -294,7 +297,7 @@ msgstr ""
#. name for acx
msgid "Arabic; Omani"
msgstr ""
msgstr "عماني"
#. name for acy
msgid "Arabic; Cypriot"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -17,14 +17,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2011-09-27 18:12+0000\n"
"PO-Revision-Date: 2013-04-21 09:31+0000\n"
"Last-Translator: Kovid Goyal <Unknown>\n"
"Language-Team: Danish <dansk@klid.dk>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-11-26 05:11+0000\n"
"X-Generator: Launchpad (build 14381)\n"
"X-Launchpad-Export-Date: 2013-04-22 05:23+0000\n"
"X-Generator: Launchpad (build 16567)\n"
"Language: da\n"
#. name for aaa

View File

@ -18,14 +18,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2013-03-15 22:01+0000\n"
"Last-Translator: Hendrik Knackstedt <Unknown>\n"
"PO-Revision-Date: 2013-04-11 13:29+0000\n"
"Last-Translator: Simon Schütte <simonschuette@arcor.de>\n"
"Language-Team: Ubuntu German Translators\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-03-16 04:55+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Launchpad-Export-Date: 2013-04-12 05:20+0000\n"
"X-Generator: Launchpad (build 16564)\n"
"Language: de\n"
#. name for aaa
@ -58,7 +58,7 @@ msgstr "Ambrak"
#. name for aah
msgid "Arapesh; Abu'"
msgstr "Arapesh;Abu' (Papua-Neuguinea)"
msgstr "Arapesh; Abu' (Papua-Neuguinea)"
#. name for aai
msgid "Arifama-Miniafia"

View File

@ -12,14 +12,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2011-09-27 17:41+0000\n"
"Last-Translator: Kovid Goyal <Unknown>\n"
"PO-Revision-Date: 2013-04-12 15:49+0000\n"
"Last-Translator: Costis Aspiotis <aspiotisk@gmail.com>\n"
"Language-Team: Greek <debian-l10n-greek@lists.debian.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-11-26 05:17+0000\n"
"X-Generator: Launchpad (build 14381)\n"
"X-Launchpad-Export-Date: 2013-04-13 05:32+0000\n"
"X-Generator: Launchpad (build 16564)\n"
"Language: el\n"
#. name for aaa
@ -30825,7 +30825,7 @@ msgstr ""
#. name for zxx
msgid "No linguistic content"
msgstr ""
msgstr "Χωρίς γλωσσολογικό περιεχόμενο"
#. name for zyb
msgid "Zhuang; Yongbei"

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, 9, 27)
numeric_version = (0, 9, 28)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
@ -29,7 +29,7 @@ isportable = os.environ.get('CALIBRE_PORTABLE_BUILD', None) is not None
ispy3 = sys.version_info.major > 2
isxp = iswindows and sys.getwindowsversion().major < 6
is64bit = sys.maxsize > (1 << 32)
isworker = os.environ.has_key('CALIBRE_WORKER') or os.environ.has_key('CALIBRE_SIMPLE_WORKER')
isworker = 'CALIBRE_WORKER' in os.environ or 'CALIBRE_SIMPLE_WORKER' in os.environ
if isworker:
os.environ.pop('CALIBRE_FORCE_ANSI', None)
@ -58,7 +58,8 @@ def get_osx_version():
return _osx_ver
filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
if filesystem_encoding is None:
filesystem_encoding = 'utf-8'
else:
try:
if codecs.lookup(filesystem_encoding).name == 'ascii':
@ -85,7 +86,7 @@ def _get_cache_dir():
confcache = os.path.join(config_dir, u'caches')
if isportable:
return confcache
if os.environ.has_key('CALIBRE_CACHE_DIRECTORY'):
if 'CALIBRE_CACHE_DIRECTORY' in os.environ:
return os.path.abspath(os.environ['CALIBRE_CACHE_DIRECTORY'])
if iswindows:
@ -184,7 +185,7 @@ if plugins is None:
CONFIG_DIR_MODE = 0700
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
if 'CALIBRE_CONFIG_DIRECTORY' in os.environ:
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
if plugins['winutil'][0] is None:

View File

@ -60,13 +60,24 @@ class TOLINO(EB600):
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['_TELEKOMTOLINO']
def linux_swap_drives(self, drives):
if len(drives) < 2 or not drives[1] or not drives[2]: return drives
if len(drives) < 2 or not drives[1] or not drives[2]:
return drives
drives = list(drives)
t = drives[0]
drives[0] = drives[1]
drives[1] = t
return tuple(drives)
def windows_sort_drives(self, drives):
if len(drives) < 2:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:
drives['main'] = carda
drives['carda'] = main
return drives
class COOL_ER(EB600):
name = 'Cool-er device interface'
@ -94,13 +105,11 @@ class SHINEBOOK(EB600):
MAIN_MEMORY_VOLUME_LABEL = 'ShineBook Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'ShineBook Storage Card'
@classmethod
def can_handle(cls, dev, debug=False):
return dev[4] == 'ShineBook'
class POCKETBOOK360(EB600):
# Device info on OS X
@ -113,7 +122,6 @@ class POCKETBOOK360(EB600):
PRODUCT_ID = [0x1688, 0xa4a5]
BCD = [0x110]
FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', 'txt']
VENDOR_NAME = ['PHILIPS', '__POCKET', 'POCKETBO']
@ -312,7 +320,8 @@ class POCKETBOOK701(USBMS):
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
def windows_sort_drives(self, drives):
if len(drives) < 2: return drives
if len(drives) < 2:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:

View File

@ -35,7 +35,7 @@ class KOBO(USBMS):
gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader')
author = 'Timothy Legge and David Forrester'
version = (2, 0, 8)
version = (2, 0, 9)
dbversion = 0
fwversion = 0
@ -1193,8 +1193,11 @@ class KOBO(USBMS):
db.set_comment(db_id, mi.comments)
# Add bookmark file to db_id
db.add_format_with_hooks(db_id, bm.value.bookmark_extension,
bm.value.path, index_is_id=True)
# NOTE: As it is, this copied the book from the device back to the library. That meant it replaced the
# existing file. Taking this out for that reason, but some books have a ANNOT file that could be
# copied.
# db.add_format_with_hooks(db_id, bm.value.bookmark_extension,
# bm.value.path, index_is_id=True)
class KOBOTOUCH(KOBO):
@ -1366,7 +1369,7 @@ class KOBOTOUCH(KOBO):
prefix = self._card_a_prefix if oncard == 'carda' else \
self._card_b_prefix if oncard == 'cardb' \
else self._main_prefix
debug_print("KoboTouch:books - prefix='%s'"%oncard)
debug_print("KoboTouch:books - oncard='%s', prefix='%s'"%(oncard, prefix))
# Determine the firmware version
try:
@ -2100,7 +2103,8 @@ class KOBOTOUCH(KOBO):
:param filepath: The full path to the ebook file
'''
# debug_print("KoboTouch:upload_cover - path='%s' filename='%s'"%(path, filename))
debug_print("KoboTouch:upload_cover - path='%s' filename='%s' "%(path, filename))
debug_print(" filepath='%s' "%(filepath))
opts = self.settings()
if not self.copying_covers():
@ -2109,7 +2113,7 @@ class KOBOTOUCH(KOBO):
return
# Only upload covers to SD card if that is supported
if self._card_a_prefix and path.startswith(self._card_a_prefix) and not self.supports_covers_on_sdcard():
if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and not self.supports_covers_on_sdcard():
return
if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]:
@ -2133,12 +2137,13 @@ class KOBOTOUCH(KOBO):
def images_path(self, path):
if self._card_a_prefix and path.startswith(self._card_a_prefix) and self.supports_covers_on_sdcard():
if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and self.supports_covers_on_sdcard():
path_prefix = 'koboExtStorage/images/'
path = self._card_a_prefix + path_prefix
path = os.path.join(self._card_a_prefix, path_prefix)
else:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix
path = os.path.join(self._main_prefix, path_prefix)
return path
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, keep_cover_aspect=False):
@ -2181,11 +2186,16 @@ class KOBOTOUCH(KOBO):
cursor.close()
if ImageID != None:
path = self.images_path(path) + ImageID
path = os.path.join(self.images_path(path), ImageID)
if show_debug:
debug_print("KoboTouch:_upload_cover - About to loop over cover endings")
image_dir = os.path.dirname(os.path.abspath(path))
if not os.path.exists(image_dir):
debug_print("KoboTouch:_upload_cover - Image directory does not exust. Creating path='%s'" % (image_dir))
os.makedirs(image_dir)
for ending, cover_options in self.cover_file_endings().items():
resize, min_dbversion, max_dbversion, isFullsize = cover_options
if show_debug:

View File

@ -8,10 +8,9 @@ __docformat__ = 'restructuredtext en'
Transform OEB content into FB2 markup
'''
import re, textwrap, uuid
from base64 import b64encode
from datetime import datetime
import re
import uuid
from lxml import etree
@ -19,6 +18,7 @@ from calibre import prepare_string_for_xml
from calibre.constants import __appname__, __version__
from calibre.utils.magick import Image
from calibre.utils.localization import lang_as_iso639_1
from calibre.ebooks.oeb.base import urlnormalize
class FB2MLizer(object):
'''
@ -138,6 +138,12 @@ class FB2MLizer(object):
if not metadata['author']:
metadata['author'] = u'<author><first-name></first-name><last-name><last-name></author>'
metadata['keywords'] = u''
tags = list(map(unicode, self.oeb_book.metadata.subject))
if tags:
tags = ', '.join(prepare_string_for_xml(x) for x in tags)
metadata['keywords'] = '<keywords>%s</keywords>'%tags
metadata['sequence'] = u''
if self.oeb_book.metadata.series:
index = '1'
@ -145,6 +151,7 @@ class FB2MLizer(object):
index = self.oeb_book.metadata.series_index[0]
metadata['sequence'] = u'<sequence name="%s" number="%s" />' % (prepare_string_for_xml(u'%s' % self.oeb_book.metadata.series[0]), index)
year = publisher = isbn = u''
identifiers = self.oeb_book.metadata['identifier']
for x in identifiers:
if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'):
@ -154,31 +161,57 @@ class FB2MLizer(object):
self.log.warn('No UUID identifier found')
metadata['id'] = str(uuid.uuid4())
try:
date = self.oeb_book.metadata['date'][0]
except IndexError:
pass
else:
year = '<year>%s</year>' % prepare_string_for_xml(date.value.partition('-')[0])
try:
publisher = self.oeb_book.metadata['publisher'][0]
except IndexError:
pass
else:
publisher = '<publisher>%s</publisher>' % prepare_string_for_xml(publisher.value)
for x in identifiers:
if x.get(OPF('scheme'), None).lower() == 'isbn':
isbn = '<isbn>%s</isbn>' % prepare_string_for_xml(x.value)
metadata['year'], metadata['isbn'], metadata['publisher'] = year, isbn, publisher
for key, value in metadata.items():
if key not in ('author', 'cover', 'sequence'):
if key not in ('author', 'cover', 'sequence', 'keywords', 'year', 'publisher', 'isbn'):
metadata[key] = prepare_string_for_xml(value)
return u'<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">' \
'<description>' \
'<title-info>' \
'<genre>%(genre)s</genre>' \
'%(author)s' \
'<book-title>%(title)s</book-title>' \
'%(cover)s' \
'<lang>%(lang)s</lang>' \
'%(sequence)s' \
'</title-info>' \
'<document-info>' \
'%(author)s' \
'<program-used>%(appname)s %(version)s</program-used>' \
'<date>%(date)s</date>' \
'<id>%(id)s</id>' \
'<version>1.0</version>' \
'</document-info>' \
'</description>' % metadata
return textwrap.dedent(u'''
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">
<description>
<title-info>
<genre>%(genre)s</genre>
%(author)s
<book-title>%(title)s</book-title>
%(cover)s
<lang>%(lang)s</lang>
%(keywords)s
%(sequence)s
</title-info>
<document-info>
%(author)s
<program-used>%(appname)s %(version)s</program-used>
<date>%(date)s</date>
<id>%(id)s</id>
<version>1.0</version>
</document-info>
<publish-info>
%(year)s
%(publisher)s
%(isbn)s
</publish-info>
</description>\n''') % metadata
def fb2_footer(self):
return u'</FictionBook>'
return u'\n</FictionBook>'
def get_cover(self):
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
@ -281,7 +314,7 @@ class FB2MLizer(object):
data += char
images.append('<binary id="%s" content-type="image/jpeg">%s\n</binary>' % (self.image_hrefs[item.href], data))
except Exception as e:
self.log.error('Error: Could not include file %s because ' \
self.log.error('Error: Could not include file %s because '
'%s.' % (item.href, e))
return ''.join(images)
@ -420,13 +453,16 @@ class FB2MLizer(object):
if tag == 'img':
if elem_tree.attrib.get('src', None):
# Only write the image tag if it is in the manifest.
if page.abshref(elem_tree.attrib['src']) in self.oeb_book.manifest.hrefs.keys():
if page.abshref(elem_tree.attrib['src']) not in self.image_hrefs.keys():
self.image_hrefs[page.abshref(elem_tree.attrib['src'])] = '_%s.jpg' % len(self.image_hrefs.keys())
ihref = urlnormalize(page.abshref(elem_tree.attrib['src']))
if ihref in self.oeb_book.manifest.hrefs:
if ihref not in self.image_hrefs:
self.image_hrefs[ihref] = '_%s.jpg' % len(self.image_hrefs)
p_txt, p_tag = self.ensure_p()
fb2_out += p_txt
tags += p_tag
fb2_out.append('<image xlink:href="#%s" />' % self.image_hrefs[page.abshref(elem_tree.attrib['src'])])
fb2_out.append('<image xlink:href="#%s" />' % self.image_hrefs[ihref])
else:
self.log.warn(u'Ignoring image not in manifest: %s'%ihref)
if tag in ('br', 'hr') or ems >= 1:
if ems < 1:
multiplier = 1

View File

@ -13,7 +13,8 @@ from calibre.customize import FileTypePlugin
from calibre.utils.zipfile import ZipFile, stringFileHeader
def is_comic(list_of_names):
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names
if '.' in x and x.lower().rpartition('/')[-1] != 'thumbs.db'])
comic_extensions = set(['jpg', 'jpeg', 'png'])
return len(extensions - comic_extensions) == 0
@ -58,7 +59,7 @@ class ArchiveExtract(FileTypePlugin):
else:
fnames = zf.namelist()
fnames = [x for x in fnames if '.' in x]
fnames = [x for x in fnames if '.' in x and x.lower().rpartition('/')[-1] != 'thumbs.db']
if is_comic(fnames):
ext = '.cbr' if is_rar else '.cbz'
of = self.temporary_file('_archive_extract'+ext)

View File

@ -46,7 +46,8 @@ def read_info(outputdir, get_cover):
ans = {}
for line in raw.splitlines():
if u':' not in line: continue
if u':' not in line:
continue
field, val = line.partition(u':')[::2]
val = val.strip()
if field and val:
@ -54,7 +55,7 @@ def read_info(outputdir, get_cover):
if get_cover:
try:
subprocess.check_call([pdftoppm, '-singlefile', '-jpeg',
subprocess.check_call([pdftoppm, '-singlefile', '-jpeg', '-cropbox',
'src.pdf', 'cover'])
except subprocess.CalledProcessError as e:
prints('pdftoppm errored out with return code: %d'%e.returncode)
@ -69,7 +70,7 @@ def page_images(pdfpath, outputdir, first=1, last=1):
import win32process as w
args['creationflags'] = w.HIGH_PRIORITY_CLASS | w.CREATE_NO_WINDOW
try:
subprocess.check_call([pdftoppm, '-jpeg', '-f', unicode(first),
subprocess.check_call([pdftoppm, '-cropbox', '-jpeg', '-f', unicode(first),
'-l', unicode(last), pdfpath,
os.path.join(outputdir, 'page-images')], **args)
except subprocess.CalledProcessError as e:

View File

@ -44,14 +44,18 @@ class Links(object):
for link in links:
href, page, rect = link
p, frag = href.partition('#')[0::2]
try:
link = ((path, p, frag or None), self.pdf.get_pageref(page).obj, Array(rect))
except IndexError:
self.log.warn('Unable to find page for link: %r, ignoring it' % link)
continue
self.links.append(link)
def add_links(self):
for link in self.links:
path, href, frag = link[0]
page, rect = link[1:]
combined_path = os.path.abspath(os.path.join(os.path.dirname(path), *unquote(href).split('/')))
combined_path = os.path.normcase(os.path.abspath(os.path.join(os.path.dirname(path), *unquote(href).split('/'))))
is_local = not href or combined_path in self.anchors
annot = Dictionary({
'Type':Name('Annot'), 'Subtype':Name('Link'),

View File

@ -66,6 +66,7 @@ class EditorWidget(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
self.readonly = False
self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)
@ -163,7 +164,11 @@ class EditorWidget(QWebView): # {{{
self.page().linkClicked.connect(self.link_clicked)
self.setHtml('')
self.page().setContentEditable(True)
self.set_readonly(False)
def set_readonly(self, what):
self.readonly = what
self.page().setContentEditable(not self.readonly)
def clear_text(self, *args):
us = self.page().undoStack()
@ -313,7 +318,7 @@ class EditorWidget(QWebView): # {{{
# toList() is needed because PyQt on Debian is old/broken
for body in self.page().mainFrame().documentElement().findAll('body').toList():
body.setAttribute('style', style)
self.page().setContentEditable(True)
self.page().setContentEditable(not self.readonly)
def keyPressEvent(self, ev):
if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
@ -585,6 +590,7 @@ class Editor(QWidget): # {{{
self.tabs.addTab(self.code_edit, _('HTML Source'))
self.tabs.currentChanged[int].connect(self.change_tab)
self.highlighter = Highlighter(self.code_edit.document())
self.layout().setContentsMargins(0, 0, 0, 0)
# toolbar1 {{{
self.toolbar1.addAction(self.editor.action_undo)
@ -666,6 +672,12 @@ class Editor(QWidget): # {{{
self.toolbar2.setVisible(False)
self.toolbar3.setVisible(False)
def set_readonly(self, what):
self.editor.set_readonly(what)
def hide_tabs(self):
self.tabs.tabBar().setVisible(False)
# }}}
if __name__ == '__main__':

View File

@ -1131,6 +1131,13 @@ class DeviceMixin(object): # {{{
# so we don't need to worry about whether some succeeded or not.
self.refresh_ondevice(reset_only=False)
try:
if not self.current_view().currentIndex().isValid():
self.current_view().set_current_row()
self.current_view().refresh_book_details()
except:
traceback.print_exc()
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:

View File

@ -23,14 +23,17 @@ class LanguagesEdit(EditWithComplete):
self.comma_map = {k:k.replace(',', '|') for k in self.names_with_commas}
self.comma_rmap = {v:k for k, v in self.comma_map.iteritems()}
self._rmap = {lower(v):k for k,v in self._lang_map.iteritems()}
if db is not None:
self.init_langs(db)
def init_langs(self, db):
if db is not None:
pmap = {self._lang_map.get(x[1], x[1]):1 for x in
db.get_languages_with_ids()}
all_items = sorted(self._lang_map.itervalues(),
key=lambda x: (-pmap.get(x, 0), sort_key(x)))
else:
all_items = sorted(self._lang_map.itervalues(),
key=lambda x: sort_key(x))
self.update_items_cache(all_items)
@property

View File

@ -173,7 +173,7 @@ class SearchBar(QWidget): # {{{
self._layout.setContentsMargins(0,5,0,0)
x = QToolButton(self)
x.setText(_('Virtual Library'))
x.setText(_('Vi&rtual Library'))
x.setIcon(QIcon(I('lt.png')))
x.setObjectName("virtual_library")
x.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)

View File

@ -485,7 +485,7 @@ class SeriesEdit(EditWithComplete):
def initialize(self, db, id_):
self.books_to_refresh = set([])
all_series = db.all_series()
all_series.sort(key=lambda x : sort_key(x[1]))
all_series.sort(key=lambda x: sort_key(x[1]))
self.update_items_cache([x[1] for x in all_series])
series_id = db.series_id(id_, index_is_id=True)
inval = ''
@ -1174,7 +1174,6 @@ class TagsEdit(EditWithComplete): # {{{
self.current_val = d.tags
self.all_items = db.all_tags()
def commit(self, db, id_):
self.books_to_refresh |= db.set_tags(
id_, self.current_val, notify=False, commit=False,
@ -1194,8 +1193,10 @@ class LanguagesEdit(LE): # {{{
@dynamic_property
def current_val(self):
def fget(self): return self.lang_codes
def fset(self, val): self.lang_codes = val
def fget(self):
return self.lang_codes
def fset(self, val):
self.lang_codes = val
return property(fget=fget, fset=fset)
def initialize(self, db, id_):
@ -1309,7 +1310,7 @@ class IdentifiersEdit(QLineEdit): # {{{
# }}}
class ISBNDialog(QDialog) : # {{{
class ISBNDialog(QDialog): # {{{
def __init__(self, parent, txt):
QDialog.__init__(self, parent)
@ -1320,7 +1321,7 @@ class ISBNDialog(QDialog) : # {{{
l.addWidget(w, 0, 0, 1, 2)
w = QLabel(_('ISBN:'))
l.addWidget(w, 1, 0, 1, 1)
self.line_edit = w = QLineEdit();
self.line_edit = w = QLineEdit()
w.setText(txt)
w.selectAll()
w.textChanged.connect(self.checkText)
@ -1388,7 +1389,7 @@ class PublisherEdit(EditWithComplete): # {{{
def initialize(self, db, id_):
self.books_to_refresh = set([])
all_publishers = db.all_publishers()
all_publishers.sort(key=lambda x : sort_key(x[1]))
all_publishers.sort(key=lambda x: sort_key(x[1]))
self.update_items_cache([x[1] for x in all_publishers])
publisher_id = db.publisher_id(id_, index_is_id=True)
inval = ''
@ -1421,7 +1422,7 @@ class DateEdit(QDateTimeEdit):
ATTR = 'timestamp'
TWEAK = 'gui_timestamp_display_format'
def __init__(self, parent):
def __init__(self, parent, create_clear_button=True):
QDateTimeEdit.__init__(self, parent)
self.setToolTip(self.TOOLTIP)
self.setWhatsThis(self.TOOLTIP)
@ -1435,6 +1436,7 @@ class DateEdit(QDateTimeEdit):
self.setCalendarWidget(self.cw)
self.setMinimumDateTime(UNDEFINED_QDATETIME)
self.setSpecialValueText(_('Undefined'))
if create_clear_button:
self.clear_button = QToolButton(parent)
self.clear_button.setIcon(QIcon(I('trash.png')))
self.clear_button.setToolTip(_('Clear date'))

View File

@ -0,0 +1,518 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from collections import OrderedDict, namedtuple
from functools import partial
from future_builtins import zip
from PyQt4.Qt import (
QDialog, QWidget, QGridLayout, QLineEdit, QLabel, QToolButton, QIcon,
QVBoxLayout, QDialogButtonBox, QApplication, pyqtSignal, QFont, QPixmap,
QSize, QPainter, Qt, QColor, QPen, QSizePolicy, QScrollArea, QFrame)
from calibre import fit_image
from calibre.ebooks.metadata import title_sort, authors_to_sort_string
from calibre.gui2 import pixmap_to_data, gprefs
from calibre.gui2.comments_editor import Editor
from calibre.gui2.languages import LanguagesEdit as LE
from calibre.gui2.metadata.basic_widgets import PubdateEdit, RatingEdit
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import UNDEFINED_DATE
Widgets = namedtuple('Widgets', 'new old label button')
# Widgets {{{
class LineEdit(QLineEdit):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
QLineEdit.__init__(self, parent)
self.is_new = is_new
self.field = field
self.metadata = metadata
if not is_new:
self.setReadOnly(True)
self.textChanged.connect(self.changed)
def from_mi(self, mi):
val = mi.get(self.field, default='') or ''
ism = self.metadata['is_multiple']
if ism:
if not val:
val = ''
else:
val = ism['list_to_ui'].join(val)
self.setText(val)
self.setCursorPosition(0)
def to_mi(self, mi):
val = unicode(self.text()).strip()
ism = self.metadata['is_multiple']
if ism:
if not val:
val = []
else:
val = [x.strip() for x in val.split(ism['list_to_ui']) if x.strip()]
mi.set(self.field, val)
if self.field == 'title':
mi.set('title_sort', title_sort(val, lang=mi.language))
elif self.field == 'authors':
mi.set('author_sort', authors_to_sort_string(val))
@dynamic_property
def current_val(self):
def fget(self):
return unicode(self.text())
def fset(self, val):
self.setText(val)
self.setCursorPosition(0)
return property(fget=fget, fset=fset)
@property
def is_blank(self):
return not self.current_val.strip()
class LanguagesEdit(LE):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
LE.__init__(self, parent=parent)
self.is_new = is_new
self.field = field
self.metadata = metadata
self.textChanged.connect(self.changed)
if not is_new:
self.lineEdit().setReadOnly(True)
@dynamic_property
def current_val(self):
def fget(self):
return self.lang_codes
def fset(self, val):
self.lang_codes = val
return property(fget=fget, fset=fset)
def from_mi(self, mi):
self.lang_codes = mi.languages
def to_mi(self, mi):
mi.languages = self.lang_codes
@property
def is_blank(self):
return not self.current_val
class RatingsEdit(RatingEdit):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
RatingEdit.__init__(self, parent)
self.is_new = is_new
self.field = field
self.metadata = metadata
self.valueChanged.connect(self.changed)
if not is_new:
self.setReadOnly(True)
def from_mi(self, mi):
val = (mi.get(self.field, default=0) or 0)/2
self.setValue(val)
def to_mi(self, mi):
mi.set(self.field, self.value() * 2)
@property
def is_blank(self):
return self.value() == 0
class DateEdit(PubdateEdit):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
PubdateEdit.__init__(self, parent, create_clear_button=False)
self.is_new = is_new
self.field = field
self.metadata = metadata
self.setDisplayFormat(extra)
self.dateTimeChanged.connect(self.changed)
if not is_new:
self.setReadOnly(True)
def from_mi(self, mi):
self.current_val = mi.get(self.field, default=None)
def to_mi(self, mi):
mi.set(self.field, self.current_val)
@property
def is_blank(self):
return self.current_val == UNDEFINED_DATE
class SeriesEdit(LineEdit):
def from_mi(self, mi):
series = mi.get(self.field, default='')
series_index = mi.get(self.field + '_index', default=1.0)
val = ''
if series:
val = '%s [%s]' % (series, mi.format_series_index(series_index))
self.setText(val)
self.setCursorPosition(0)
def to_mi(self, mi):
val = unicode(self.text()).strip()
try:
series_index = float(val.rpartition('[')[-1].rstrip(']').strip())
except:
series_index = 1.0
series = val.rpartition('[')[0].strip() or None
mi.set(self.field, series)
mi.set(self.field + '_index', series_index)
class IdentifiersEdit(LineEdit):
def from_mi(self, mi):
val = ('%s:%s' % (k, v) for k, v in mi.identifiers.iteritems())
self.setText(', '.join(val))
self.setCursorPosition(0)
def to_mi(self, mi):
parts = (x.strip() for x in self.current_val.split(',') if x.strip())
val = {x.partition(':')[0].strip():x.partition(':')[-1].strip() for x in parts}
mi.set_identifiers({k:v for k, v in val.iteritems() if k and v})
class CommentsEdit(Editor):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
Editor.__init__(self, parent, one_line_toolbar=False)
self.is_new = is_new
self.field = field
self.metadata = metadata
self.hide_tabs()
if not is_new:
self.hide_toolbars()
self.set_readonly(True)
@dynamic_property
def current_val(self):
def fget(self):
return self.html
def fset(self, val):
self.html = val or ''
self.changed.emit()
return property(fget=fget, fset=fset)
def from_mi(self, mi):
val = mi.get(self.field, default='')
self.current_val = val
def to_mi(self, mi):
mi.set(self.field, self.current_val)
def sizeHint(self):
return QSize(450, 200)
@property
def is_blank(self):
return not self.current_val.strip()
class CoverView(QWidget):
changed = pyqtSignal()
def __init__(self, field, is_new, parent, metadata, extra):
QWidget.__init__(self, parent)
self.is_new = is_new
self.field = field
self.metadata = metadata
self.pixmap = None
self.blank = QPixmap(I('blank.png'))
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.GrowFlag|QSizePolicy.ExpandFlag)
self.sizePolicy().setHeightForWidth(True)
@property
def is_blank(self):
return self.pixmap is None
@dynamic_property
def current_val(self):
def fget(self):
return self.pixmap
def fset(self, val):
self.pixmap = val
self.changed.emit()
self.update()
return property(fget=fget, fset=fset)
def from_mi(self, mi):
p = getattr(mi, 'cover', None)
if p and os.path.exists(p):
pmap = QPixmap()
with open(p, 'rb') as f:
pmap.loadFromData(f.read())
if not pmap.isNull():
self.pixmap = pmap
self.update()
self.changed.emit()
return
cd = getattr(mi, 'cover_data', (None, None))
if cd and cd[1]:
pmap = QPixmap()
pmap.loadFromData(cd[1])
if not pmap.isNull():
self.pixmap = pmap
self.update()
self.changed.emit()
return
self.pixmap = None
self.update()
self.changed.emit()
def to_mi(self, mi):
mi.cover, mi.cover_data = None, (None, None)
if self.pixmap is not None and not self.pixmap.isNull():
with PersistentTemporaryFile('.jpg') as pt:
pt.write(pixmap_to_data(self.pixmap))
mi.cover = pt.name
def sizeHint(self):
return QSize(225, 300)
def paintEvent(self, event):
pmap = self.blank if self.pixmap is None or self.pixmap.isNull() else self.pixmap
target = self.rect()
scaled, width, height = fit_image(pmap.width(), pmap.height(), target.width(), target.height())
target.setRect(target.x(), target.y(), width, height)
p = QPainter(self)
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
p.drawPixmap(target, pmap)
if self.pixmap is not None and not self.pixmap.isNull():
sztgt = target.adjusted(0, 0, 0, -4)
f = p.font()
f.setBold(True)
p.setFont(f)
sz = u'\u00a0%d x %d\u00a0'%(self.pixmap.width(), self.pixmap.height())
flags = Qt.AlignBottom|Qt.AlignRight|Qt.TextSingleLine
szrect = p.boundingRect(sztgt, flags, sz)
p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200))
p.setPen(QPen(QColor(255,255,255)))
p.drawText(sztgt, flags, sz)
p.end()
# }}}
class CompareSingle(QWidget):
def __init__(
self, field_metadata, parent=None, revert_tooltip=None,
datetime_fmt='MMMM yyyy', blank_as_equal=True,
fields=('title', 'authors', 'series', 'tags', 'rating', 'publisher', 'pubdate', 'identifiers', 'languages', 'comments', 'cover')):
QWidget.__init__(self, parent)
self.l = l = QGridLayout()
l.setContentsMargins(0, 0, 0, 0)
self.setLayout(l)
revert_tooltip = revert_tooltip or _('Revert %s')
self.current_mi = None
self.changed_font = QFont(QApplication.font())
self.changed_font.setBold(True)
self.changed_font.setItalic(True)
self.blank_as_equal = blank_as_equal
self.widgets = OrderedDict()
row = 0
for field in fields:
m = field_metadata[field]
dt = m['datatype']
extra = None
if 'series' in {field, dt}:
cls = SeriesEdit
elif field == 'identifiers':
cls = IdentifiersEdit
elif field == 'languages':
cls = LanguagesEdit
elif 'comments' in {field, dt}:
cls = CommentsEdit
elif 'rating' in {field, dt}:
cls = RatingsEdit
elif dt == 'datetime':
extra = datetime_fmt
cls = DateEdit
elif field == 'cover':
cls = CoverView
elif dt in {'text', 'enum'}:
cls = LineEdit
else:
continue
neww = cls(field, True, self, m, extra)
neww.changed.connect(partial(self.changed, field))
oldw = cls(field, False, self, m, extra)
newl = QLabel('&%s:' % m['name'])
newl.setBuddy(neww)
button = QToolButton(self)
button.setIcon(QIcon(I('back.png')))
button.clicked.connect(partial(self.revert, field))
button.setToolTip(revert_tooltip % m['name'])
self.widgets[field] = Widgets(neww, oldw, newl, button)
for i, w in enumerate((newl, neww, button, oldw)):
c = i if i < 2 else i + 1
if w is oldw:
c += 1
l.addWidget(w, row, c)
row += 1
self.sep = f = QFrame(self)
f.setFrameShape(f.VLine)
l.addWidget(f, 0, 2, row, 1)
self.sep2 = f = QFrame(self)
f.setFrameShape(f.VLine)
l.addWidget(f, 0, 4, row, 1)
def changed(self, field):
w = self.widgets[field]
if w.new.current_val != w.old.current_val and (not self.blank_as_equal or not w.new.is_blank):
w.label.setFont(self.changed_font)
else:
w.label.setFont(QApplication.font())
def revert(self, field):
widgets = self.widgets[field]
neww, oldw = widgets[:2]
neww.current_val = oldw.current_val
def __call__(self, oldmi, newmi):
self.current_mi = newmi
self.initial_vals = {}
for field, widgets in self.widgets.iteritems():
widgets.old.from_mi(oldmi)
widgets.new.from_mi(newmi)
self.initial_vals[field] = widgets.new.current_val
def apply_changes(self):
changed = False
for field, widgets in self.widgets.iteritems():
val = widgets.new.current_val
if val != self.initial_vals[field]:
widgets.new.to_mi(self.current_mi)
changed = True
return changed
class CompareMany(QDialog):
def __init__(self, ids, get_metadata, field_metadata, parent=None, window_title=None, skip_button_tooltip=None,
accept_all_tooltip=None, reject_all_tooltip=None, **kwargs):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.setWindowIcon(QIcon(I('auto_author_sort.png')))
self.get_metadata = get_metadata
self.ids = list(ids)
self.total = len(self.ids)
self.accepted = OrderedDict()
self.window_title = window_title or _('Compare metadata')
self.compare_widget = CompareSingle(field_metadata, parent=parent, **kwargs)
self.sa = sa = QScrollArea()
l.addWidget(sa)
sa.setWidget(self.compare_widget)
sa.setWidgetResizable(True)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Cancel)
bb.rejected.connect(self.reject)
self.aarb = b = bb.addButton(_('&Accept all remaining'), bb.YesRole)
if accept_all_tooltip:
b.setToolTip(accept_all_tooltip)
b.clicked.connect(self.accept_all_remaining)
self.rarb = b = bb.addButton(_('Re&ject all remaining'), bb.NoRole)
if reject_all_tooltip:
b.setToolTip(reject_all_tooltip)
b.clicked.connect(self.reject_all_remaining)
self.sb = b = bb.addButton(_('&Reject'), bb.ActionRole)
b.clicked.connect(partial(self.next_item, False))
if skip_button_tooltip:
b.setToolTip(skip_button_tooltip)
self.nb = b = bb.addButton(_('&Next'), bb.ActionRole)
b.setIcon(QIcon(I('forward.png')))
b.clicked.connect(partial(self.next_item, True))
b.setDefault(True)
l.addWidget(bb)
self.next_item(True)
desktop = QApplication.instance().desktop()
geom = desktop.availableGeometry(parent or self)
width = max(700, min(950, geom.width()-50))
height = max(650, min(1000, geom.height()-100))
self.resize(QSize(width, height))
geom = gprefs.get('diff_dialog_geom', None)
if geom is not None:
self.restoreGeometry(geom)
def accept(self):
gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry()))
super(CompareMany, self).accept()
def reject(self):
gprefs.set('diff_dialog_geom', bytearray(self.saveGeometry()))
super(CompareMany, self).reject()
@property
def current_mi(self):
return self.compare_widget.current_mi
def next_item(self, accept):
if not self.ids:
return self.accept()
if self.current_mi is not None:
changed = self.compare_widget.apply_changes()
self.setWindowTitle(self.window_title + _(' [%(num)d of %(tot)d]') % dict(
num=(self.total - len(self.ids) + 1), tot=self.total))
oldmi, newmi = self.get_metadata(self.ids[0])
old_id = self.ids.pop(0)
if self.current_mi is not None:
self.accepted[old_id] = (changed, self.current_mi) if accept else (False, None)
self.compare_widget(oldmi, newmi)
def accept_all_remaining(self):
self.next_item(True)
for id_ in self.ids:
oldmi, newmi = self.get_metadata(id_)
self.accepted[id_] = (False, newmi)
self.ids = []
self.accept()
def reject_all_remaining(self):
self.next_item(False)
for id_ in self.ids:
oldmi, newmi = self.get_metadata(id_)
self.accepted[id_] = (False, None)
self.ids = []
self.accept()
if __name__ == '__main__':
app = QApplication([])
from calibre.library import db
db = db()
ids = sorted(db.all_ids(), reverse=True)
ids = tuple(zip(ids[0::2], ids[1::2]))
gm = partial(db.get_metadata, index_is_id=True, get_cover=True, cover_as_data=True)
get_metadata = lambda x:map(gm, ids[x])
d = CompareMany(list(xrange(len(ids))), get_metadata, db.field_metadata)
if d.exec_() == d.Accepted:
for changed, mi in d.accepted.itervalues():
if changed and mi is not None:
print (mi)

View File

@ -8,7 +8,7 @@ from functools import partial
from PyQt4.Qt import (
Qt, QMenu, QPoint, QIcon, QDialog, QGridLayout, QLabel, QLineEdit, QComboBox,
QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList, QCheckBox)
QDialogButtonBox, QSize, QVBoxLayout, QListWidget, QStringList, QRadioButton)
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.widgets import ComboBoxWithHelp
@ -31,7 +31,10 @@ class SelectNames(QDialog): # {{{
self._names.setSelectionMode(self._names.ExtendedSelection)
l.addWidget(self._names)
self._and = QCheckBox(_('Match all selected %s names')%txt)
self._or = QRadioButton(_('Match any of the selected %s names')%txt)
self._and = QRadioButton(_('Match all of the selected %s names')%txt)
self._or.setChecked(True)
l.addWidget(self._or)
l.addWidget(self._and)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
@ -308,7 +311,7 @@ class SearchRestrictionMixin(object):
self.virtual_library.clicked.connect(self.virtual_library_clicked)
self.virtual_library_tooltip = \
_('Books display will show only those books matching the search')
_('Use a "virtual library" to show only a subset of the books present in this library')
self.virtual_library.setToolTip(self.virtual_library_tooltip)
self.search_restriction = ComboBoxWithHelp(self)
@ -318,7 +321,6 @@ class SearchRestrictionMixin(object):
self.edit_menu = QMenu(_('Edit Virtual Library'))
self.rm_menu = QMenu(_('Remove Virtual Library'))
def add_virtual_library(self, db, name, search):
virt_libs = db.prefs.get('virtual_libraries', {})
virt_libs[name] = search

View File

@ -7,7 +7,6 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import random
import urllib
from contextlib import closing

View File

@ -60,7 +60,7 @@ class TagBrowserMixin(object): # {{{
self.do_saved_search_edit, (None,), 'search')
):
m = self.alter_tb.manage_menu
m.addAction( QIcon(I(category_icon_map[cat_name])), text,
m.addAction(QIcon(I(category_icon_map[cat_name])), text,
partial(func, *args))
def do_restriction_error(self):
@ -280,7 +280,7 @@ class TagBrowserMixin(object): # {{{
def do_tag_item_renamed(self):
# Clean up library view and search
# get information to redo the selection
rows = [r.row() for r in \
rows = [r.row() for r in
self.library_view.selectionModel().selectedRows()]
m = self.library_view.model()
ids = [m.id(r) for r in rows]
@ -407,22 +407,24 @@ class TagBrowserWidget(QWidget): # {{{
a = sb.m.addAction(x)
sb.bg.addAction(a)
a.setCheckable(True)
if i == 0: a.setChecked(True)
if i == 0:
a.setChecked(True)
sb.setToolTip(
_('Set the sort order for entries in the Tag Browser'))
sb.setStatusTip(sb.toolTip())
ma = l.m.addAction(_('Match type'))
ma = l.m.addAction(_('Search type when selecting multiple items'))
ma.m = l.match_menu = QMenu(l.m)
ma.setMenu(ma.m)
ma.ag = QActionGroup(ma)
# Must be in the same order as db2.MATCH_TYPE
for i, x in enumerate((_('Match any'), _('Match all'))):
for i, x in enumerate((_('Match any of the items'), _('Match all of the items'))):
a = ma.m.addAction(x)
ma.ag.addAction(a)
a.setCheckable(True)
if i == 0: a.setChecked(True)
if i == 0:
a.setChecked(True)
ma.setToolTip(
_('When selecting multiple entries in the Tag Browser '
'match any or all of them'))

View File

@ -573,7 +573,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed(recount=False) # reload the search restrictions combo box
self.apply_named_search_restriction(db.prefs['gui_restriction'])
if db.prefs['virtual_lib_on_startup']:
self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
for action in self.iactions.values():
action.library_changed(db)
if olddb is not None:

View File

@ -252,7 +252,7 @@ class FieldMetadata(dict):
'datatype':'int',
'is_multiple':{},
'kind':'field',
'name':None,
'name':_('Cover'),
'search_terms':['cover'],
'is_custom':False,
'is_category':False,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More