mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
8e797e0120
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')]
|
||||
|
||||
|
352
setup/installer/windows/libimobiledevice_notes.rst
Normal file
352
setup/installer/windows/libimobiledevice_notes.rst
Normal 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/
|
||||
|
@ -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
|
||||
---------
|
||||
|
||||
|
@ -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
2402
setup/iso_639/cs.po
2402
setup/iso_639/cs.po
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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'),
|
||||
|
@ -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__':
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
518
src/calibre/gui2/metadata/diff.py
Normal file
518
src/calibre/gui2/metadata/diff.py
Normal 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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'))
|
||||
|
@ -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:
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user