mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.9.14
This commit is contained in:
commit
01df610d1a
@ -19,6 +19,68 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
- version: 0.9.14
|
||||
date: 2012-01-11
|
||||
|
||||
new features:
|
||||
- title: "When adding multiple books and duplicates are found, allow the user to select which of the duplicate books will be added anyway."
|
||||
tickets: [1095256]
|
||||
|
||||
- title: "Device drivers for Kobo Arc on linux, Polaroid Abdroid tablet"
|
||||
tickets: [1098049]
|
||||
|
||||
- title: "When sorting by series, use the language of the book to decide what leading articles to remove, just as is done for sorting by title"
|
||||
|
||||
bug fixes:
|
||||
- title: "PDF Output: Do not error out when the input document contains links with anchors not present in the document."
|
||||
tickets: [1096428]
|
||||
|
||||
- title: "Add support for upgraded db on newest Kobo firmware"
|
||||
tickets: [1095617]
|
||||
|
||||
- title: "PDF Output: Fix typo that broke use of custom paper sizes."
|
||||
tickets: [1097563]
|
||||
|
||||
- title: "PDF Output: Handle empty anchors present at the end of a page"
|
||||
|
||||
- title: "PDF Output: Fix side margins of last page in a flow being incorrect when large side margins are used."
|
||||
tickets: [1096290]
|
||||
|
||||
- title: "Edit metadata dialog: Allow setting the series number for custom series type columns to zero"
|
||||
|
||||
- title: "When bulk editing custom series-type columns and not provding a series number use 1 as the default, instead of None"
|
||||
|
||||
- title: "Catalogs: Fix issue with catalog generation using Hungarian UI and author_sort beginning with multiple letter groups."
|
||||
tickets: [1091581]
|
||||
|
||||
- title: "PDF Output: Dont error out on files that have invalid font-family declarations."
|
||||
tickets: [1096279]
|
||||
|
||||
- title: "Do not load QRawFont at global level, to allow calibre installation on systems with missing dependencies"
|
||||
tickets: [1096170]
|
||||
|
||||
- title: "PDF Output: Fix cover not present in generated PDF files"
|
||||
tickets: [1096098]
|
||||
|
||||
improved recipes:
|
||||
- Sueddeutsche Zeitung mobil
|
||||
- Boerse Online
|
||||
- TidBits
|
||||
- New York Review of Books
|
||||
- Fleshbot
|
||||
- Il Messaggero
|
||||
- Libero
|
||||
|
||||
new recipes:
|
||||
- title: Spectator Magazine, Oxford Mail and Outside Magazine
|
||||
author: Krittika Goyal
|
||||
|
||||
- title: Libartes
|
||||
author: Darko Miletic
|
||||
|
||||
- title: El Diplo
|
||||
author: Tomas De Domenico
|
||||
|
||||
- version: 0.9.13
|
||||
date: 2013-01-04
|
||||
|
||||
|
@ -672,6 +672,19 @@ There are three possible things I know of, that can cause this:
|
||||
* The Logitech SetPoint Settings application causes random crashes in
|
||||
|app| when it is open. Close it before starting |app|.
|
||||
|
||||
If none of the above apply to you, then there is some other program on your
|
||||
computer that is interfering with |app|. First reboot your computer is safe
|
||||
mode, to have as few running programs as possible, and see if the crashes still
|
||||
happen. If they do not, then you know it is some program causing the problem.
|
||||
The most likely such culprit is a program that modifies other programs'
|
||||
behavior, such as an antivirus, a device driver, something like RoboForm (an
|
||||
automatic form filling app) or an assistive technology like Voice Control or a
|
||||
Screen Reader.
|
||||
|
||||
The only way to find the culprit is to eliminate the programs one by one and
|
||||
see which one is causing the issue. Basically, stop a program, run calibre,
|
||||
check for crashes. If they still happen, stop another program and repeat.
|
||||
|
||||
|app| is not starting on OS X?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -1,33 +1,36 @@
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
class AdvancedUserRecipe1303841067(BasicNewsRecipe):
|
||||
|
||||
title = u'Börse-online'
|
||||
__author__ = 'schuster'
|
||||
oldest_article = 1
|
||||
title = u'Börse-online'
|
||||
__author__ = 'schuster, Armin Geller'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
language = 'de'
|
||||
remove_javascript = True
|
||||
cover_url = 'http://www.dpv.de/images/1995/source.gif'
|
||||
masthead_url = 'http://www.zeitschriften-cover.de/cover/boerse-online-cover-januar-2010-x1387.jpg'
|
||||
extra_css = '''
|
||||
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||
h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||
img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
|
||||
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||
'''
|
||||
remove_tags_bevor = [dict(name='h3')]
|
||||
remove_tags_after = [dict(name='div', attrs={'class':'artikelfuss'})]
|
||||
remove_tags = [dict(attrs={'class':['moduleTopNav', 'moduleHeaderNav', 'text', 'blau', 'poll1150']}),
|
||||
dict(id=['newsletterlayer', 'newsletterlayerClose', 'newsletterlayer_body', 'newsletterarray_error', 'newsletterlayer_emailadress', 'newsletterlayer_submit', 'kommentar']),
|
||||
dict(name=['h2', 'Gesamtranking', 'h3',''])]
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
language = 'de'
|
||||
remove_javascript = True
|
||||
encoding = 'iso-8859-1'
|
||||
timefmt = ' [%a, %d %b %Y]'
|
||||
|
||||
|
||||
cover_url = 'http://www.wirtschaftsmedien-shop.de/s/media/coverimages/7576_2013107.jpg'
|
||||
masthead_url = 'http://upload.wikimedia.org/wikipedia/de/5/56/B%C3%B6rse_Online_Logo.svg'
|
||||
|
||||
remove_tags_after = [dict(name='div', attrs={'class':['artikelfuss', 'rahmen600']})]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['breadcrumb', 'rightCol', 'clearall']}),
|
||||
dict(name='div', attrs={'class':['footer', 'artikelfuss']}),
|
||||
]
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'id':['contentWrapper']})
|
||||
]
|
||||
|
||||
feeds = [(u'Börsennachrichten', u'http://www.boerse-online.de/rss/')]
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace('.html#nv=rss', '.html?mode=print')
|
||||
|
||||
|
||||
|
||||
feeds = [(u'Börsennachrichten', u'http://www.boerse-online.de/rss/')]
|
||||
|
||||
|
@ -11,16 +11,15 @@ class BusinessWeekMagazine(BasicNewsRecipe):
|
||||
category = 'news'
|
||||
encoding = 'UTF-8'
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'id':'article_body_container'}),
|
||||
]
|
||||
remove_tags = [dict(name='ui'),dict(name='li')]
|
||||
dict(name='div', attrs={'id':'article_body_container'}),
|
||||
]
|
||||
remove_tags = [dict(name='ui'),dict(name='li'),dict(name='div', attrs={'id':['share-email']})]
|
||||
no_javascript = True
|
||||
no_stylesheets = True
|
||||
|
||||
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
|
||||
|
||||
def parse_index(self):
|
||||
|
||||
#Go to the issue
|
||||
soup = self.index_to_soup('http://www.businessweek.com/magazine/news/articles/business_news.htm')
|
||||
|
||||
@ -47,7 +46,6 @@ class BusinessWeekMagazine(BasicNewsRecipe):
|
||||
if section_title not in feeds:
|
||||
feeds[section_title] = []
|
||||
feeds[section_title] += articles
|
||||
|
||||
div1 = soup.find ('div', attrs={'class':'column center'})
|
||||
section_title = ''
|
||||
for div in div1.findAll('h5'):
|
||||
|
118
recipes/el_diplo.recipe
Normal file
118
recipes/el_diplo.recipe
Normal file
@ -0,0 +1,118 @@
|
||||
# Copyright 2013 Tomás Di Domenico
|
||||
#
|
||||
# This is a news fetching recipe for the Calibre ebook software, for
|
||||
# fetching the Cono Sur edition of Le Monde Diplomatique (www.eldiplo.org).
|
||||
#
|
||||
# This recipe is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this recipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
from contextlib import closing
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.magick import Image
|
||||
|
||||
class ElDiplo_Recipe(BasicNewsRecipe):
|
||||
title = u'El Diplo'
|
||||
__author__ = 'Tomas Di Domenico'
|
||||
description = 'Publicacion mensual de Le Monde Diplomatique, edicion Argentina'
|
||||
langauge = 'es_AR'
|
||||
needs_subscription = True
|
||||
auto_cleanup = True
|
||||
|
||||
def get_cover(self,url):
|
||||
tmp_cover = PersistentTemporaryFile(suffix = ".jpg", prefix = "eldiplo_")
|
||||
self.cover_url = tmp_cover.name
|
||||
|
||||
with closing(self.browser.open(url)) as r:
|
||||
imgdata = r.read()
|
||||
|
||||
img = Image()
|
||||
img.load(imgdata)
|
||||
img.crop(img.size[0],img.size[1]/2,0,0)
|
||||
|
||||
img.save(tmp_cover.name)
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('http://www.eldiplo.org/index.php/login/-/do_login/index.html')
|
||||
br.select_form(nr=3)
|
||||
br['uName'] = self.username
|
||||
br['uPassword'] = self.password
|
||||
br.submit()
|
||||
self.browser = br
|
||||
return br
|
||||
|
||||
def parse_index(self):
|
||||
default_sect = 'General'
|
||||
articles = {default_sect:[]}
|
||||
ans = [default_sect]
|
||||
sectionsmarker = 'DOSSIER_TITLE: '
|
||||
sectionsre = re.compile('^'+sectionsmarker)
|
||||
|
||||
soup = self.index_to_soup('http://www.eldiplo.org/index.php')
|
||||
|
||||
coverdivs = soup.findAll(True,attrs={'id':['lmd-foto']})
|
||||
a = coverdivs[0].find('a', href=True)
|
||||
coverurl = a['href'].split("?imagen=")[1]
|
||||
self.get_cover(coverurl)
|
||||
|
||||
thedivs = soup.findAll(True,attrs={'class':['lmd-leermas']})
|
||||
for div in thedivs:
|
||||
a = div.find('a', href=True)
|
||||
if 'Sumario completo' in self.tag_to_string(a, use_alt=True):
|
||||
summaryurl = re.sub(r'\?.*', '', a['href'])
|
||||
summaryurl = 'http://www.eldiplo.org' + summaryurl
|
||||
|
||||
for pagenum in xrange(1,10):
|
||||
soup = self.index_to_soup('{0}/?cms1_paging_p_b32={1}'.format(summaryurl,pagenum))
|
||||
thedivs = soup.findAll(True,attrs={'class':['interna']})
|
||||
|
||||
if len(thedivs) == 0:
|
||||
break
|
||||
|
||||
for div in thedivs:
|
||||
section = div.find(True,text=sectionsre).replace(sectionsmarker,'')
|
||||
if section == '':
|
||||
section = default_sect
|
||||
|
||||
if section not in articles.keys():
|
||||
articles[section] = []
|
||||
ans.append(section)
|
||||
|
||||
nota = div.find(True,attrs={'class':['lmd-pl-titulo-nota-dossier']})
|
||||
a = nota.find('a', href=True)
|
||||
if not a:
|
||||
continue
|
||||
|
||||
url = re.sub(r'\?.*', '', a['href'])
|
||||
url = 'http://www.eldiplo.org' + url
|
||||
title = self.tag_to_string(a, use_alt=True).strip()
|
||||
|
||||
summary = div.find(True, attrs={'class':'lmd-sumario-descript'}).find('p')
|
||||
if summary:
|
||||
description = self.tag_to_string(summary, use_alt=False)
|
||||
|
||||
aut = div.find(True, attrs={'class':'lmd-autor-sumario'})
|
||||
if aut:
|
||||
auth = self.tag_to_string(aut, use_alt=False).strip()
|
||||
|
||||
if not articles.has_key(section):
|
||||
articles[section] = []
|
||||
|
||||
articles[section].append(dict(title=title,author=auth,url=url,date=None,description=description,content=''))
|
||||
|
||||
#ans = self.sort_index_by(ans, {'The Front Page':-1, 'Dining In, Dining Out':1, 'Obituaries':2})
|
||||
ans = [(section, articles[section]) for section in ans if articles.has_key(section)]
|
||||
return ans
|
@ -18,7 +18,7 @@ class Fleshbot(BasicNewsRecipe):
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/kotaku.com/img/logo.png'
|
||||
masthead_url = 'http://fbassets.s3.amazonaws.com/images/uploads/2012/01/fleshbot-logo.png'
|
||||
extra_css = '''
|
||||
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
|
||||
img{margin-bottom: 1em}
|
||||
@ -31,7 +31,7 @@ class Fleshbot(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/fleshbot/vip?format=xml')]
|
||||
feeds = [(u'Articles', u'http://www.fleshbot.com/feed')]
|
||||
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
|
BIN
recipes/icons/libartes.png
Normal file
BIN
recipes/icons/libartes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 282 B |
@ -28,12 +28,15 @@ class IlMessaggero(BasicNewsRecipe):
|
||||
recursion = 10
|
||||
|
||||
remove_javascript = True
|
||||
extra_css = ' .bianco31lucida{color: black} '
|
||||
|
||||
|
||||
keep_only_tags = [dict(name='h1', attrs={'class':'titoloLettura2'}),
|
||||
dict(name='h2', attrs={'class':'sottotitLettura'}),
|
||||
dict(name='span', attrs={'class':'testoArticoloG'})
|
||||
keep_only_tags = [dict(name='h1', attrs={'class':['titoloLettura2','titoloart','bianco31lucida']}),
|
||||
dict(name='h2', attrs={'class':['sottotitLettura','grigio16']}),
|
||||
dict(name='span', attrs={'class':'testoArticoloG'}),
|
||||
dict(name='div', attrs={'id':'testodim'})
|
||||
]
|
||||
|
||||
|
||||
def get_cover_url(self):
|
||||
cover = None
|
||||
st = time.localtime()
|
||||
@ -55,17 +58,16 @@ class IlMessaggero(BasicNewsRecipe):
|
||||
feeds = [
|
||||
(u'HomePage', u'http://www.ilmessaggero.it/rss/home.xml'),
|
||||
(u'Primo Piano', u'http://www.ilmessaggero.it/rss/initalia_primopiano.xml'),
|
||||
(u'Cronaca Bianca', u'http://www.ilmessaggero.it/rss/initalia_cronacabianca.xml'),
|
||||
(u'Cronaca Nera', u'http://www.ilmessaggero.it/rss/initalia_cronacanera.xml'),
|
||||
(u'Economia e Finanza', u'http://www.ilmessaggero.it/rss/economia.xml'),
|
||||
(u'Politica', u'http://www.ilmessaggero.it/rss/initalia_politica.xml'),
|
||||
(u'Scienza e Tecnologia', u'http://www.ilmessaggero.it/rss/scienza.xml'),
|
||||
(u'Cinema', u'http://www.ilmessaggero.it/rss.php?refresh_ce#'),
|
||||
(u'Viaggi', u'http://www.ilmessaggero.it/rss.php?refresh_ce#'),
|
||||
(u'Cultura', u'http://www.ilmessaggero.it/rss/cultura.xml'),
|
||||
(u'Tecnologia', u'http://www.ilmessaggero.it/rss/tecnologia.xml'),
|
||||
(u'Spettacoli', u'http://www.ilmessaggero.it/rss/spettacoli.xml'),
|
||||
(u'Edizioni Locali', u'http://www.ilmessaggero.it/rss/edlocali.xml'),
|
||||
(u'Roma', u'http://www.ilmessaggero.it/rss/roma.xml'),
|
||||
(u'Cultura e Tendenze', u'http://www.ilmessaggero.it/rss/roma_culturaspet.xml'),
|
||||
(u'Benessere', u'http://www.ilmessaggero.it/rss/benessere.xml'),
|
||||
(u'Sport', u'http://www.ilmessaggero.it/rss/sport.xml'),
|
||||
(u'Calcio', u'http://www.ilmessaggero.it/rss/sport_calcio.xml'),
|
||||
(u'Motori', u'http://www.ilmessaggero.it/rss/sport_motori.xml')
|
||||
(u'Moda', u'http://www.ilmessaggero.it/rss/moda.xml')
|
||||
]
|
||||
|
||||
|
||||
|
69
recipes/libartes.recipe
Normal file
69
recipes/libartes.recipe
Normal file
@ -0,0 +1,69 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
libartes.com
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Libartes(BasicNewsRecipe):
|
||||
title = 'Libartes'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Elektronski časopis Libartes delo je kulturnih entuzijasta, umetnika i teoretičara umetnosti i književnosti. Časopis Libartes izlazi tromesečno i bavi se različitim granama umetnosti - književnošću, muzikom, filmom, likovnim umetnostima, dizajnom i arhitekturom.'
|
||||
publisher = 'Libartes'
|
||||
category = 'literatura, knjizevnost, film, dizajn, arhitektura, muzika'
|
||||
no_stylesheets = True
|
||||
INDEX = 'http://libartes.com/'
|
||||
use_embedded_content = False
|
||||
encoding = 'utf-8'
|
||||
language = 'sr'
|
||||
publication_type = 'magazine'
|
||||
masthead_url = 'http://libartes.com/index_files/logo.gif'
|
||||
extra_css = """
|
||||
@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
||||
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||
body{font-family: "Times New Roman",Times,serif1, serif}
|
||||
img{display:block}
|
||||
.naslov{font-size: xx-large; font-weight: bold}
|
||||
.nag{font-size: large; font-weight: bold}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
remove_tags_before = dict(attrs={'id':'nav'})
|
||||
remove_tags_after = dict(attrs={'id':'fb' })
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'center_content'})]
|
||||
remove_tags = [
|
||||
dict(name=['object','link','iframe','embed','meta'])
|
||||
,dict(attrs={'id':'nav'})
|
||||
]
|
||||
|
||||
def parse_index(self):
|
||||
articles = []
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
for item in soup.findAll(name='a', attrs={'class':'belad'}, href=True):
|
||||
feed_link = item
|
||||
if feed_link['href'].startswith(self.INDEX):
|
||||
url = feed_link['href']
|
||||
else:
|
||||
url = self.INDEX + feed_link['href']
|
||||
|
||||
title = self.tag_to_string(feed_link)
|
||||
date = strftime(self.timefmt)
|
||||
articles.append({
|
||||
'title' :title
|
||||
,'date' :date
|
||||
,'url' :url
|
||||
,'description':''
|
||||
})
|
||||
return [('Casopis Libartes', articles)]
|
||||
|
@ -14,7 +14,8 @@ class LiberoNews(BasicNewsRecipe):
|
||||
__author__ = 'Marini Gabriele'
|
||||
description = 'Italian daily newspaper'
|
||||
|
||||
cover_url = 'http://www.libero-news.it/images/logo.png'
|
||||
#cover_url = 'http://www.liberoquotidiano.it/images/Libero%20Quotidiano.jpg'
|
||||
cover_url = 'http://www.edicola.liberoquotidiano.it/vnlibero/fpcut.jsp?testata=milano'
|
||||
title = u'Libero '
|
||||
publisher = 'EDITORIALE LIBERO s.r.l 2006'
|
||||
category = 'News, politics, culture, economy, general interest'
|
||||
@ -32,10 +33,11 @@ class LiberoNews(BasicNewsRecipe):
|
||||
remove_javascript = True
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':'Articolo'})
|
||||
dict(name='div', attrs={'class':'Articolo'}),
|
||||
dict(name='article')
|
||||
]
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['CommentaFoto','Priva2']}),
|
||||
dict(name='div', attrs={'class':['CommentaFoto','Priva2','login_commenti','box_16']}),
|
||||
dict(name='div', attrs={'id':['commentigenerale']})
|
||||
]
|
||||
feeds = [
|
||||
|
@ -66,21 +66,22 @@ class NewYorkReviewOfBooks(BasicNewsRecipe):
|
||||
self.log('Issue date:', date)
|
||||
|
||||
# Find TOC
|
||||
toc = soup.find('ul', attrs={'class':'issue-article-list'})
|
||||
tocs = soup.findAll('ul', attrs={'class':'issue-article-list'})
|
||||
articles = []
|
||||
for li in toc.findAll('li'):
|
||||
h3 = li.find('h3')
|
||||
title = self.tag_to_string(h3)
|
||||
author = self.tag_to_string(li.find('h4'))
|
||||
title = title + u' (%s)'%author
|
||||
url = 'http://www.nybooks.com'+h3.find('a', href=True)['href']
|
||||
desc = ''
|
||||
for p in li.findAll('p'):
|
||||
desc += self.tag_to_string(p)
|
||||
self.log('Found article:', title)
|
||||
self.log('\t', url)
|
||||
self.log('\t', desc)
|
||||
articles.append({'title':title, 'url':url, 'date':'',
|
||||
for toc in tocs:
|
||||
for li in toc.findAll('li'):
|
||||
h3 = li.find('h3')
|
||||
title = self.tag_to_string(h3)
|
||||
author = self.tag_to_string(li.find('h4'))
|
||||
title = title + u' (%s)'%author
|
||||
url = 'http://www.nybooks.com'+h3.find('a', href=True)['href']
|
||||
desc = ''
|
||||
for p in li.findAll('p'):
|
||||
desc += self.tag_to_string(p)
|
||||
self.log('Found article:', title)
|
||||
self.log('\t', url)
|
||||
self.log('\t', desc)
|
||||
articles.append({'title':title, 'url':url, 'date':'',
|
||||
'description':desc})
|
||||
|
||||
return [('Current Issue', articles)]
|
||||
|
65
recipes/outside_magazine.recipe
Normal file
65
recipes/outside_magazine.recipe
Normal file
@ -0,0 +1,65 @@
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class NYTimes(BasicNewsRecipe):
|
||||
|
||||
title = 'Outside Magazine'
|
||||
__author__ = 'Krittika Goyal'
|
||||
description = 'Outside Magazine - Free 1 Month Old Issue'
|
||||
timefmt = ' [%d %b, %Y]'
|
||||
needs_subscription = False
|
||||
language = 'en'
|
||||
|
||||
no_stylesheets = True
|
||||
#auto_cleanup = True
|
||||
#auto_cleanup_keep = '//div[@class="thumbnail"]'
|
||||
|
||||
keep_only_tags = dict(name='div', attrs={'class':'masonry-box width-four'})
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['share-bar', 'outbrain_widget_0', 'outbrain_widget_1', 'livefyre']}),
|
||||
#dict(name='div', attrs={'id':['qrformdiv', 'inSection', 'alpha-inner']}),
|
||||
#dict(name='form', attrs={'onsubmit':''}),
|
||||
dict(name='section', attrs={'id':['article-quote', 'article-navigation']}),
|
||||
]
|
||||
#TO GET ARTICLE TOC
|
||||
def out_get_index(self):
|
||||
super_url = 'http://www.outsideonline.com/magazine/'
|
||||
super_soup = self.index_to_soup(super_url)
|
||||
div = super_soup.find(attrs={'class':'masonry-box width-four'})
|
||||
issue = div.findAll(name='article')[1]
|
||||
super_a = issue.find('a', href=True)
|
||||
return super_a.get('href')
|
||||
|
||||
|
||||
# To parse artice toc
|
||||
def parse_index(self):
|
||||
parse_soup = self.index_to_soup(self.out_get_index())
|
||||
|
||||
feeds = []
|
||||
feed_title = 'Articles'
|
||||
|
||||
articles = []
|
||||
self.log('Found section:', feed_title)
|
||||
div = parse_soup.find(attrs={'class':'print clearfix'})
|
||||
for art in div.findAll(name='p'):
|
||||
art_info = art.find(name = 'a')
|
||||
if art_info is None:
|
||||
continue
|
||||
art_title = self.tag_to_string(art_info)
|
||||
url = art_info.get('href') + '?page=all'
|
||||
self.log.info('\tFound article:', art_title, 'at', url)
|
||||
article = {'title':art_title, 'url':url, 'date':''}
|
||||
#au = art.find(attrs={'class':'articleAuthors'})
|
||||
#if au is not None:
|
||||
#article['author'] = self.tag_to_string(au)
|
||||
#desc = art.find(attrs={'class':'hover_text'})
|
||||
#if desc is not None:
|
||||
#desc = self.tag_to_string(desc)
|
||||
#if 'author' in article:
|
||||
#desc = ' by ' + article['author'] + ' ' +desc
|
||||
#article['description'] = desc
|
||||
articles.append(article)
|
||||
if articles:
|
||||
feeds.append((feed_title, articles))
|
||||
|
||||
return feeds
|
||||
|
22
recipes/oxford_mail.recipe
Normal file
22
recipes/oxford_mail.recipe
Normal file
@ -0,0 +1,22 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class HindustanTimes(BasicNewsRecipe):
|
||||
title = u'Oxford Mail'
|
||||
language = 'en_GB'
|
||||
__author__ = 'Krittika Goyal'
|
||||
oldest_article = 1 #days
|
||||
max_articles_per_feed = 25
|
||||
#encoding = 'cp1252'
|
||||
use_embedded_content = False
|
||||
|
||||
no_stylesheets = True
|
||||
auto_cleanup = True
|
||||
|
||||
|
||||
feeds = [
|
||||
('News',
|
||||
'http://www.oxfordmail.co.uk/news/rss/'),
|
||||
('Sports',
|
||||
'http://www.oxfordmail.co.uk/sport/rss/'),
|
||||
]
|
||||
|
@ -6,7 +6,6 @@ class PhilosophyNow(BasicNewsRecipe):
|
||||
|
||||
title = 'Philosophy Now'
|
||||
__author__ = 'Rick Shang'
|
||||
|
||||
description = '''Philosophy Now is a lively magazine for everyone
|
||||
interested in ideas. It isn't afraid to tackle all the major questions of
|
||||
life, the universe and everything. Published every two months, it tries to
|
||||
@ -27,7 +26,7 @@ class PhilosophyNow(BasicNewsRecipe):
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
br.open('https://philosophynow.org/auth/login')
|
||||
br.select_form(nr = 1)
|
||||
br.select_form(name="loginForm")
|
||||
br['username'] = self.username
|
||||
br['password'] = self.password
|
||||
br.submit()
|
||||
@ -50,19 +49,20 @@ class PhilosophyNow(BasicNewsRecipe):
|
||||
#Go to the main body
|
||||
current_issue_url = 'http://philosophynow.org/issues/' + issuenum
|
||||
soup = self.index_to_soup(current_issue_url)
|
||||
div = soup.find ('div', attrs={'class':'articlesColumn'})
|
||||
div = soup.find ('div', attrs={'class':'contentsColumn'})
|
||||
|
||||
feeds = OrderedDict()
|
||||
|
||||
for post in div.findAll('h3'):
|
||||
|
||||
for post in div.findAll('h1'):
|
||||
articles = []
|
||||
a=post.find('a',href=True)
|
||||
if a is not None:
|
||||
url="http://philosophynow.org" + a['href']
|
||||
title=self.tag_to_string(a).strip()
|
||||
s=post.findPrevious('h4')
|
||||
s=post.findPrevious('h3')
|
||||
section_title = self.tag_to_string(s).strip()
|
||||
d=post.findNext('p')
|
||||
d=post.findNext('h2')
|
||||
desc = self.tag_to_string(d).strip()
|
||||
articles.append({'title':title, 'url':url, 'description':desc, 'date':''})
|
||||
|
||||
@ -73,3 +73,5 @@ class PhilosophyNow(BasicNewsRecipe):
|
||||
ans = [(key, val) for key, val in feeds.iteritems()]
|
||||
return ans
|
||||
|
||||
def cleanup(self):
|
||||
self.browser.open('http://philosophynow.org/auth/logout')
|
||||
|
60
recipes/spectator_magazine.recipe
Normal file
60
recipes/spectator_magazine.recipe
Normal file
@ -0,0 +1,60 @@
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class NYTimes(BasicNewsRecipe):
|
||||
|
||||
title = 'Spectator Magazine'
|
||||
__author__ = 'Krittika Goyal'
|
||||
description = 'Magazine'
|
||||
timefmt = ' [%d %b, %Y]'
|
||||
needs_subscription = False
|
||||
language = 'en'
|
||||
|
||||
no_stylesheets = True
|
||||
#auto_cleanup = True
|
||||
#auto_cleanup_keep = '//div[@class="thumbnail"]'
|
||||
|
||||
keep_only_tags = dict(name='div', attrs={'id':'content'})
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['disqus_thread']}),
|
||||
##dict(name='div', attrs={'id':['qrformdiv', 'inSection', 'alpha-inner']}),
|
||||
##dict(name='form', attrs={'onsubmit':''}),
|
||||
#dict(name='section', attrs={'id':['article-quote', 'article-navigation']}),
|
||||
]
|
||||
|
||||
#TO GET ARTICLE TOC
|
||||
def spec_get_index(self):
|
||||
return self.index_to_soup('http://www.spectator.co.uk/')
|
||||
|
||||
# To parse artice toc
|
||||
def parse_index(self):
|
||||
parse_soup = self.index_to_soup('http://www.spectator.co.uk/')
|
||||
|
||||
feeds = []
|
||||
feed_title = 'Spectator Magazine Articles'
|
||||
|
||||
articles = []
|
||||
self.log('Found section:', feed_title)
|
||||
div = parse_soup.find(attrs={'class':'one-col-tax-widget magazine-list columns-1 post-8 taxonomy-category full-width widget section-widget icit-taxonomical-listings'})
|
||||
for art in div.findAll(name='h2'):
|
||||
art_info = art.find(name = 'a')
|
||||
if art_info is None:
|
||||
continue
|
||||
art_title = self.tag_to_string(art_info)
|
||||
url = art_info.get('href')
|
||||
self.log.info('\tFound article:', art_title, 'at', url)
|
||||
article = {'title':art_title, 'url':url, 'date':''}
|
||||
#au = art.find(attrs={'class':'articleAuthors'})
|
||||
#if au is not None:
|
||||
#article['author'] = self.tag_to_string(au)
|
||||
#desc = art.find(attrs={'class':'hover_text'})
|
||||
#if desc is not None:
|
||||
#desc = self.tag_to_string(desc)
|
||||
#if 'author' in article:
|
||||
#desc = ' by ' + article['author'] + ' ' +desc
|
||||
#article['description'] = desc
|
||||
articles.append(article)
|
||||
if articles:
|
||||
feeds.append((feed_title, articles))
|
||||
|
||||
return feeds
|
||||
|
@ -1,13 +1,16 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Andreas Zeiser <andreas.zeiser@web.de>'
|
||||
__copyright__ = '2012, 2013 Andreas Zeiser <andreas.zeiser@web.de>'
|
||||
'''
|
||||
szmobil.sueddeutsche.de/
|
||||
'''
|
||||
# History
|
||||
# 2013.01.09 Fixed bugs in article titles containing "strong" and
|
||||
# other small changes
|
||||
# 2012.08.04 Initial release
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
import re
|
||||
import re
|
||||
|
||||
class SZmobil(BasicNewsRecipe):
|
||||
title = u'Süddeutsche Zeitung mobil'
|
||||
@ -26,6 +29,8 @@ class SZmobil(BasicNewsRecipe):
|
||||
delay = 1
|
||||
cover_source = 'http://www.sueddeutsche.de/verlag'
|
||||
|
||||
# if you want to get rid of the date on the title page use
|
||||
# timefmt = ''
|
||||
timefmt = ' [%a, %d %b, %Y]'
|
||||
|
||||
root_url ='http://szmobil.sueddeutsche.de/'
|
||||
@ -50,7 +55,7 @@ class SZmobil(BasicNewsRecipe):
|
||||
|
||||
return browser
|
||||
|
||||
def parse_index(self):
|
||||
def parse_index(self):
|
||||
# find all sections
|
||||
src = self.index_to_soup('http://szmobil.sueddeutsche.de')
|
||||
feeds = []
|
||||
@ -76,10 +81,10 @@ class SZmobil(BasicNewsRecipe):
|
||||
# first check if link is a special article in section "Meinungsseite"
|
||||
if itt.find('strong')!= None:
|
||||
article_name = itt.strong.string
|
||||
article_shorttitle = itt.contents[1]
|
||||
if len(itt.contents)>1:
|
||||
shorttitles[article_id] = itt.contents[1]
|
||||
|
||||
articles.append( (article_name, article_url, article_id) )
|
||||
shorttitles[article_id] = article_shorttitle
|
||||
continue
|
||||
|
||||
|
||||
@ -89,7 +94,7 @@ class SZmobil(BasicNewsRecipe):
|
||||
else:
|
||||
article_name = itt.string
|
||||
|
||||
if (article_name[0:10] == " mehr"):
|
||||
if (article_name.find(" mehr") == 0):
|
||||
# just another link ("mehr") to an article
|
||||
continue
|
||||
|
||||
@ -102,7 +107,9 @@ class SZmobil(BasicNewsRecipe):
|
||||
for article_name, article_url, article_id in articles:
|
||||
url = self.root_url + article_url
|
||||
title = article_name
|
||||
pubdate = strftime('%a, %d %b')
|
||||
# if you want to get rid of date for each article use
|
||||
# pubdate = strftime('')
|
||||
pubdate = strftime('[%a, %d %b]')
|
||||
description = ''
|
||||
if shorttitles.has_key(article_id):
|
||||
description = shorttitles[article_id]
|
||||
@ -115,3 +122,4 @@ class SZmobil(BasicNewsRecipe):
|
||||
|
||||
return all_articles
|
||||
|
||||
|
||||
|
@ -16,8 +16,9 @@ class TidBITS(BasicNewsRecipe):
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
#auto_cleanup = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = True
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
remove_empty_feeds = True
|
||||
masthead_url = 'http://db.tidbits.com/images/tblogo9.gif'
|
||||
@ -30,9 +31,11 @@ class TidBITS(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
remove_tags = [dict(name='small')]
|
||||
remove_tags_after = dict(name='small')
|
||||
#remove_attributes = ['width','height']
|
||||
#remove_tags = [dict(name='small')]
|
||||
#remove_tags_after = dict(name='small')
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'center_ajax_sub'})]
|
||||
remove_tags = [dict(name='div', attrs={'id':'social-media'})]
|
||||
|
||||
feeds = [
|
||||
(u'Business Apps' , u'http://db.tidbits.com/feeds/business.rss' )
|
||||
|
Binary file not shown.
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = u'calibre'
|
||||
numeric_version = (0, 9, 13)
|
||||
numeric_version = (0, 9, 14)
|
||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
|
@ -214,7 +214,7 @@ class ANDROID(USBMS):
|
||||
'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD',
|
||||
'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0',
|
||||
'COBY_MID', 'VS', 'AINOL', 'TOPWISE', 'PAD703', 'NEXT8D12',
|
||||
'MEDIATEK']
|
||||
'MEDIATEK', 'KEENHI']
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||
@ -234,7 +234,8 @@ class ANDROID(USBMS):
|
||||
'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE',
|
||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
||||
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
||||
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS']
|
||||
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
|
||||
'ICS']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||
|
@ -37,7 +37,7 @@ class KOBO(USBMS):
|
||||
|
||||
dbversion = 0
|
||||
fwversion = 0
|
||||
supported_dbversion = 65
|
||||
supported_dbversion = 75
|
||||
has_kepubs = False
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
@ -20,6 +20,9 @@ const calibre_device_entry_t calibre_mtp_device_table[] = {
|
||||
, { "Google", 0x18d1, "Nexus 10", 0x4ee2, DEVICE_FLAGS_ANDROID_BUGS}
|
||||
, { "Google", 0x18d1, "Nexus 10", 0x4ee1, DEVICE_FLAGS_ANDROID_BUGS}
|
||||
|
||||
// Kobo Arc
|
||||
, { "Kobo", 0x2237, "Arc", 0xd108, DEVICE_FLAGS_ANDROID_BUGS}
|
||||
|
||||
, { NULL, 0xffff, NULL, 0xffff, DEVICE_FLAG_NONE }
|
||||
};
|
||||
|
||||
|
@ -13,7 +13,7 @@ from collections import namedtuple
|
||||
from functools import partial
|
||||
|
||||
from calibre import prints, as_unicode
|
||||
from calibre.constants import plugins
|
||||
from calibre.constants import plugins, islinux
|
||||
from calibre.ptempfile import SpooledTemporaryFile
|
||||
from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice
|
||||
from calibre.devices.mtp.base import MTPDeviceBase, synchronous, debug
|
||||
@ -44,6 +44,17 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
self.blacklisted_devices = set()
|
||||
self.ejected_devices = set()
|
||||
self.currently_connected_dev = None
|
||||
self._is_device_mtp = None
|
||||
if islinux:
|
||||
from calibre.devices.mtp.unix.sysfs import MTPDetect
|
||||
self._is_device_mtp = MTPDetect()
|
||||
|
||||
def is_device_mtp(self, d, debug=None):
|
||||
''' Returns True iff the _is_device_mtp check returns True and libmtp
|
||||
is able to probe the device successfully. '''
|
||||
if self._is_device_mtp is None: return False
|
||||
return (self._is_device_mtp(d, debug=debug) and
|
||||
self.libmtp.is_mtp_device(d.busnum, d.devnum))
|
||||
|
||||
def set_debug_level(self, lvl):
|
||||
self.libmtp.set_debug_level(lvl)
|
||||
@ -77,7 +88,9 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
for d in devs:
|
||||
ans = cache.get(d, None)
|
||||
if ans is None:
|
||||
ans = (d.vendor_id, d.product_id) in self.known_devices
|
||||
ans = (
|
||||
(d.vendor_id, d.product_id) in self.known_devices or
|
||||
self.is_device_mtp(d))
|
||||
cache[d] = ans
|
||||
if ans:
|
||||
return d
|
||||
@ -95,12 +108,13 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
err = 'startup() not called on this device driver'
|
||||
p(err)
|
||||
return False
|
||||
devs = [d for d in devices_on_system if (d.vendor_id, d.product_id)
|
||||
in self.known_devices and d.vendor_id != APPLE]
|
||||
devs = [d for d in devices_on_system if
|
||||
( (d.vendor_id, d.product_id) in self.known_devices or
|
||||
self.is_device_mtp(d, debug=p)) and d.vendor_id != APPLE]
|
||||
if not devs:
|
||||
p('No known MTP devices connected to system')
|
||||
p('No MTP devices connected to system')
|
||||
return False
|
||||
p('Known MTP devices connected:')
|
||||
p('MTP devices connected:')
|
||||
for d in devs: p(d)
|
||||
|
||||
for d in devs:
|
||||
|
@ -662,13 +662,6 @@ is_mtp_device(PyObject *self, PyObject *args) {
|
||||
|
||||
if (!PyArg_ParseTuple(args, "ii", &busnum, &devnum)) return NULL;
|
||||
|
||||
/*
|
||||
* LIBMTP_Check_Specific_Device does not seem to work at least on my linux
|
||||
* system. Need to investigate why later. Most devices are in the device
|
||||
* table so this is not terribly important.
|
||||
*/
|
||||
/* LIBMTP_Set_Debug(LIBMTP_DEBUG_ALL); */
|
||||
/* printf("Calling check: %d %d\n", busnum, devnum); */
|
||||
Py_BEGIN_ALLOW_THREADS;
|
||||
ans = LIBMTP_Check_Specific_Device(busnum, devnum);
|
||||
Py_END_ALLOW_THREADS;
|
||||
|
53
src/calibre/devices/mtp/unix/sysfs.py
Normal file
53
src/calibre/devices/mtp/unix/sysfs.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, glob
|
||||
|
||||
class MTPDetect(object):
|
||||
|
||||
SYSFS_PATH = os.environ.get('SYSFS_PATH', '/sys')
|
||||
|
||||
def __init__(self):
|
||||
self.base = os.path.join(self.SYSFS_PATH, 'subsystem', 'usb', 'devices')
|
||||
if not os.path.exists(self.base):
|
||||
self.base = os.path.join(self.SYSFS_PATH, 'bus', 'usb', 'devices')
|
||||
self.ok = os.path.exists(self.base)
|
||||
|
||||
def __call__(self, dev, debug=None):
|
||||
'''
|
||||
Check if the device has an interface named "MTP" using sysfs, which
|
||||
avoids probing the device.
|
||||
'''
|
||||
if not self.ok: return False
|
||||
|
||||
def read(x):
|
||||
try:
|
||||
with open(x, 'rb') as f:
|
||||
return f.read()
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
ipath = os.path.join(self.base, '{0}-*/{0}-*/interface'.format(dev.busnum))
|
||||
for x in glob.glob(ipath):
|
||||
raw = read(x)
|
||||
if not raw or raw.strip() != b'MTP': continue
|
||||
raw = read(os.path.join(os.path.dirname(os.path.dirname(x)),
|
||||
'devnum'))
|
||||
try:
|
||||
if raw and int(raw) == dev.devnum:
|
||||
if debug is not None:
|
||||
debug('Unknown device {0} claims to be an MTP device'
|
||||
.format(dev))
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
|
@ -10,9 +10,7 @@ Convert OEB ebook format to PDF.
|
||||
|
||||
import glob, os
|
||||
|
||||
from PyQt4.Qt import QRawFont, QFont
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre.constants import iswindows, islinux
|
||||
from calibre.customize.conversion import (OutputFormatPlugin,
|
||||
OptionRecommendation)
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
@ -75,13 +73,13 @@ class PDFOutput(OutputFormatPlugin):
|
||||
' of stretching it to fill the full first page of the'
|
||||
' generated pdf.')),
|
||||
OptionRecommendation(name='pdf_serif_family',
|
||||
recommended_value='Times New Roman', help=_(
|
||||
recommended_value='Liberation Serif' if islinux else 'Times New Roman', help=_(
|
||||
'The font family used to render serif fonts')),
|
||||
OptionRecommendation(name='pdf_sans_family',
|
||||
recommended_value='Helvetica', help=_(
|
||||
recommended_value='Liberation Sans' if islinux else 'Helvetica', help=_(
|
||||
'The font family used to render sans-serif fonts')),
|
||||
OptionRecommendation(name='pdf_mono_family',
|
||||
recommended_value='Courier New', help=_(
|
||||
recommended_value='Liberation Mono' if islinux else 'Courier New', help=_(
|
||||
'The font family used to render monospaced fonts')),
|
||||
OptionRecommendation(name='pdf_standard_font', choices=['serif',
|
||||
'sans', 'mono'],
|
||||
@ -104,6 +102,10 @@ class PDFOutput(OutputFormatPlugin):
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
from calibre.gui2 import must_use_qt, load_builtin_fonts
|
||||
must_use_qt()
|
||||
load_builtin_fonts()
|
||||
|
||||
self.oeb = oeb_book
|
||||
self.input_plugin, self.opts, self.log = input_plugin, opts, log
|
||||
self.output_path = output_path
|
||||
@ -137,9 +139,8 @@ class PDFOutput(OutputFormatPlugin):
|
||||
If you ever move to Qt WebKit 2.3+ then this will be unnecessary.
|
||||
'''
|
||||
from calibre.ebooks.oeb.base import urlnormalize
|
||||
from calibre.gui2 import must_use_qt
|
||||
from calibre.utils.fonts.utils import remove_embed_restriction
|
||||
from PyQt4.Qt import QFontDatabase, QByteArray
|
||||
from PyQt4.Qt import QFontDatabase, QByteArray, QRawFont, QFont
|
||||
|
||||
# First find all @font-face rules and remove them, adding the embedded
|
||||
# fonts to Qt
|
||||
@ -167,7 +168,6 @@ class PDFOutput(OutputFormatPlugin):
|
||||
raw = remove_embed_restriction(raw)
|
||||
except:
|
||||
continue
|
||||
must_use_qt()
|
||||
fid = QFontDatabase.addApplicationFontFromData(QByteArray(raw))
|
||||
family_name = None
|
||||
if fid > -1:
|
||||
@ -192,7 +192,10 @@ class PDFOutput(OutputFormatPlugin):
|
||||
if ff is None: continue
|
||||
val = ff.propertyValue
|
||||
for i in xrange(val.length):
|
||||
k = icu_lower(val[i].value)
|
||||
try:
|
||||
k = icu_lower(val[i].value)
|
||||
except (AttributeError, TypeError):
|
||||
val[i].value = k = 'times'
|
||||
if k in family_map:
|
||||
val[i].value = family_map[k]
|
||||
if iswindows:
|
||||
|
@ -36,7 +36,15 @@ class SubsetFonts(object):
|
||||
self.oeb.manifest.remove(font['item'])
|
||||
font['rule'].parentStyleSheet.deleteRule(font['rule'])
|
||||
|
||||
fonts = {}
|
||||
for font in self.embedded_fonts:
|
||||
item, chars = font['item'], font['chars']
|
||||
if item.href in fonts:
|
||||
fonts[item.href]['chars'] |= chars
|
||||
else:
|
||||
fonts[item.href] = font
|
||||
|
||||
for font in fonts.itervalues():
|
||||
if not font['chars']:
|
||||
self.log('The font %s is unused. Removing it.'%font['src'])
|
||||
remove(font)
|
||||
|
@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import codecs, zlib
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
|
||||
from calibre.constants import plugins, ispy3
|
||||
|
||||
@ -65,14 +66,20 @@ def fmtnum(o):
|
||||
def serialize(o, stream):
|
||||
if isinstance(o, float):
|
||||
stream.write_raw(pdf_float(o).encode('ascii'))
|
||||
elif isinstance(o, bool):
|
||||
# Must check bool before int as bools are subclasses of int
|
||||
stream.write_raw(b'true' if o else b'false')
|
||||
elif isinstance(o, (int, long)):
|
||||
stream.write_raw(icb(o))
|
||||
elif hasattr(o, 'pdf_serialize'):
|
||||
o.pdf_serialize(stream)
|
||||
elif o is None:
|
||||
stream.write_raw(b'null')
|
||||
elif isinstance(o, bool):
|
||||
stream.write_raw(b'true' if o else b'false')
|
||||
elif isinstance(o, datetime):
|
||||
val = o.strftime("D:%Y%m%d%H%M%%02d%z")%min(59, o.second)
|
||||
if datetime.tzinfo is not None:
|
||||
val = "(%s'%s')"%(val[:-2], val[-2:])
|
||||
stream.write(val.encode('ascii'))
|
||||
else:
|
||||
raise ValueError('Unknown object: %r'%o)
|
||||
|
||||
|
@ -52,7 +52,6 @@ class PdfEngine(QPaintEngine):
|
||||
FEATURES = QPaintEngine.AllFeatures & ~(
|
||||
QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform
|
||||
| QPaintEngine.ObjectBoundingModeGradients
|
||||
| QPaintEngine.LinearGradientFill
|
||||
| QPaintEngine.RadialGradientFill
|
||||
| QPaintEngine.ConicalGradientFill
|
||||
)
|
||||
@ -82,7 +81,7 @@ class PdfEngine(QPaintEngine):
|
||||
self.bottom_margin) / self.pixel_height
|
||||
|
||||
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
|
||||
self.graphics = Graphics()
|
||||
self.graphics = Graphics(self.pixel_width, self.pixel_height)
|
||||
self.errors_occurred = False
|
||||
self.errors, self.debug = errors, debug
|
||||
self.fonts = {}
|
||||
@ -239,7 +238,7 @@ class PdfEngine(QPaintEngine):
|
||||
|
||||
@store_error
|
||||
def drawTextItem(self, point, text_item):
|
||||
# super(PdfEngine, self).drawTextItem(point, text_item)
|
||||
# return super(PdfEngine, self).drawTextItem(point, text_item)
|
||||
self.apply_graphics_state()
|
||||
gi = self.qt_hack.get_glyphs(point, text_item)
|
||||
if not gi.indices:
|
||||
@ -247,7 +246,10 @@ class PdfEngine(QPaintEngine):
|
||||
return
|
||||
name = hash(bytes(gi.name))
|
||||
if name not in self.fonts:
|
||||
self.fonts[name] = self.create_sfnt(text_item)
|
||||
try:
|
||||
self.fonts[name] = self.create_sfnt(text_item)
|
||||
except UnsupportedFont:
|
||||
return super(PdfEngine, self).drawTextItem(point, text_item)
|
||||
metrics = self.fonts[name]
|
||||
for glyph_id in gi.indices:
|
||||
try:
|
||||
@ -342,8 +344,8 @@ class PdfDevice(QPaintDevice): # {{{
|
||||
return int(round(self.body_height * self.ydpi / 72.0))
|
||||
return 0
|
||||
|
||||
def end_page(self):
|
||||
self.engine.end_page()
|
||||
def end_page(self, *args, **kwargs):
|
||||
self.engine.end_page(*args, **kwargs)
|
||||
|
||||
def init_page(self):
|
||||
self.engine.init_page()
|
||||
|
@ -47,7 +47,7 @@ def get_page_size(opts, for_comic=False): # {{{
|
||||
if opts.unit == 'devicepixel':
|
||||
factor = 72.0 / opts.output_profile.dpi
|
||||
else:
|
||||
{'point':1.0, 'inch':inch, 'cicero':cicero,
|
||||
factor = {'point':1.0, 'inch':inch, 'cicero':cicero,
|
||||
'didot':didot, 'pica':pica, 'millimeter':mm,
|
||||
'centimeter':cm}[opts.unit]
|
||||
page_size = (factor*width, factor*height)
|
||||
@ -147,9 +147,10 @@ class PDFWriter(QObject):
|
||||
opts = self.opts
|
||||
page_size = get_page_size(self.opts)
|
||||
xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY()
|
||||
# We cannot set the side margins in the webview as there is no right
|
||||
# margin for the last page (the margins are implemented with
|
||||
# -webkit-column-gap)
|
||||
ml, mr = opts.margin_left, opts.margin_right
|
||||
margin_side = min(ml, mr)
|
||||
ml, mr = ml - margin_side, mr - margin_side
|
||||
self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml,
|
||||
top_margin=0, right_margin=mr, bottom_margin=0,
|
||||
xdpi=xdpi, ydpi=ydpi, errors=self.log.error,
|
||||
@ -162,9 +163,7 @@ class PDFWriter(QObject):
|
||||
self.total_items = len(items)
|
||||
|
||||
mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom))
|
||||
ms = self.doc.to_px(margin_side, vertical=False)
|
||||
self.margin_top, self.margin_size, self.margin_bottom = map(
|
||||
lambda x:int(floor(x)), (mt, ms, mb))
|
||||
self.margin_top, self.margin_bottom = map(lambda x:int(floor(x)), (mt, mb))
|
||||
|
||||
self.painter = QPainter(self.doc)
|
||||
self.doc.set_metadata(title=pdf_metadata.title,
|
||||
@ -176,6 +175,7 @@ class PDFWriter(QObject):
|
||||
p = QPixmap()
|
||||
p.loadFromData(self.cover_data)
|
||||
if not p.isNull():
|
||||
self.doc.init_page()
|
||||
draw_image_page(QRect(0, 0, self.doc.width(), self.doc.height()),
|
||||
self.painter, p,
|
||||
preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio)
|
||||
@ -184,7 +184,8 @@ class PDFWriter(QObject):
|
||||
self.painter.restore()
|
||||
|
||||
QTimer.singleShot(0, self.render_book)
|
||||
self.loop.exec_()
|
||||
if self.loop.exec_() == 1:
|
||||
raise Exception('PDF Output failed, see log for details')
|
||||
|
||||
if self.toc is not None and len(self.toc) > 0:
|
||||
self.doc.add_outline(self.toc)
|
||||
@ -257,7 +258,7 @@ class PDFWriter(QObject):
|
||||
paged_display.layout();
|
||||
paged_display.fit_images();
|
||||
py_bridge.value = book_indexing.all_links_and_anchors();
|
||||
'''%(self.margin_top, self.margin_size, self.margin_bottom))
|
||||
'''%(self.margin_top, 0, self.margin_bottom))
|
||||
|
||||
amap = self.bridge_value
|
||||
if not isinstance(amap, dict):
|
||||
@ -278,6 +279,7 @@ class PDFWriter(QObject):
|
||||
if self.doc.errors_occurred:
|
||||
break
|
||||
|
||||
self.doc.add_links(self.current_item, start_page, amap['links'],
|
||||
amap['anchors'])
|
||||
if not self.doc.errors_occurred:
|
||||
self.doc.add_links(self.current_item, start_page, amap['links'],
|
||||
amap['anchors'])
|
||||
|
||||
|
125
src/calibre/ebooks/pdf/render/gradients.py
Normal file
125
src/calibre/ebooks/pdf/render/gradients.py
Normal file
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
from future_builtins import map
|
||||
from collections import namedtuple
|
||||
|
||||
import sip
|
||||
from PyQt4.Qt import QLinearGradient, QPointF
|
||||
|
||||
from calibre.ebooks.pdf.render.common import Name, Array, Dictionary
|
||||
|
||||
Stop = namedtuple('Stop', 't color')
|
||||
|
||||
class LinearGradientPattern(Dictionary):
|
||||
|
||||
def __init__(self, brush, matrix, pdf, pixel_page_width, pixel_page_height):
|
||||
self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(),
|
||||
matrix.dx(), matrix.dy())
|
||||
gradient = sip.cast(brush.gradient(), QLinearGradient)
|
||||
|
||||
start, stop, stops = self.spread_gradient(gradient, pixel_page_width,
|
||||
pixel_page_height, matrix)
|
||||
|
||||
# TODO: Handle colors with different opacities
|
||||
self.const_opacity = stops[0].color[-1]
|
||||
|
||||
funcs = Array()
|
||||
bounds = Array()
|
||||
encode = Array()
|
||||
|
||||
for i, current_stop in enumerate(stops):
|
||||
if i < len(stops) - 1:
|
||||
next_stop = stops[i+1]
|
||||
func = Dictionary({
|
||||
'FunctionType': 2,
|
||||
'Domain': Array([0, 1]),
|
||||
'C0': Array(current_stop.color[:3]),
|
||||
'C1': Array(next_stop.color[:3]),
|
||||
'N': 1,
|
||||
})
|
||||
funcs.append(func)
|
||||
encode.extend((0, 1))
|
||||
if i+1 < len(stops) - 1:
|
||||
bounds.append(next_stop.t)
|
||||
|
||||
func = Dictionary({
|
||||
'FunctionType': 3,
|
||||
'Domain': Array([stops[0].t, stops[-1].t]),
|
||||
'Functions': funcs,
|
||||
'Bounds': bounds,
|
||||
'Encode': encode,
|
||||
})
|
||||
|
||||
shader = Dictionary({
|
||||
'ShadingType': 2,
|
||||
'ColorSpace': Name('DeviceRGB'),
|
||||
'AntiAlias': True,
|
||||
'Coords': Array([start.x(), start.y(), stop.x(), stop.y()]),
|
||||
'Function': func,
|
||||
'Extend': Array([True, True]),
|
||||
})
|
||||
|
||||
Dictionary.__init__(self, {
|
||||
'Type': Name('Pattern'),
|
||||
'PatternType': 2,
|
||||
'Shading': shader,
|
||||
'Matrix': Array(self.matrix),
|
||||
})
|
||||
|
||||
self.cache_key = (self.__class__.__name__, self.matrix,
|
||||
tuple(shader['Coords']), stops)
|
||||
|
||||
def spread_gradient(self, gradient, pixel_page_width, pixel_page_height,
|
||||
matrix):
|
||||
start = gradient.start()
|
||||
stop = gradient.finalStop()
|
||||
stops = list(map(lambda x: [x[0], x[1].getRgbF()], gradient.stops()))
|
||||
spread = gradient.spread()
|
||||
if False and spread != gradient.PadSpread:
|
||||
# TODO: Finish this implementation
|
||||
inv = matrix.inverted()[0]
|
||||
page_rect = tuple(map(inv.map, (
|
||||
QPointF(0, 0), QPointF(pixel_page_width, 0), QPointF(0, pixel_page_height),
|
||||
QPointF(pixel_page_width, pixel_page_height))))
|
||||
maxx = maxy = -sys.maxint-1
|
||||
minx = miny = sys.maxint
|
||||
|
||||
for p in page_rect:
|
||||
minx, maxx = min(minx, p.x()), max(maxx, p.x())
|
||||
miny, maxy = min(miny, p.y()), max(maxy, p.y())
|
||||
|
||||
def in_page(point):
|
||||
return (minx <= point.x() <= maxx and miny <= point.y() <= maxy)
|
||||
|
||||
offset = stop - start
|
||||
llimit, rlimit = start, stop
|
||||
|
||||
reflect = False
|
||||
base_stops = list(stops)
|
||||
reversed_stops = list(reversed(stops))
|
||||
do_reflect = spread == gradient.ReflectSpread
|
||||
# totl = abs(stops[-1][0] - stops[0][0])
|
||||
# intervals = [abs(stops[i+1] - stops[i])/totl for i in xrange(len(stops)-1)]
|
||||
|
||||
while in_page(llimit):
|
||||
reflect ^= True
|
||||
llimit -= offset
|
||||
estops = reversed_stops if (reflect and do_reflect) else base_stops
|
||||
stops = estops + stops
|
||||
|
||||
while in_page(rlimit):
|
||||
reflect ^= True
|
||||
rlimit += offset
|
||||
estops = reversed_stops if (reflect and do_reflect) else base_stops
|
||||
stops = stops + estops
|
||||
|
||||
return start, stop, tuple(Stop(s[0], s[1]) for s in stops)
|
||||
|
@ -11,11 +11,12 @@ from math import sqrt
|
||||
from collections import namedtuple
|
||||
|
||||
from PyQt4.Qt import (
|
||||
QBrush, QPen, Qt, QPointF, QTransform, QPainterPath, QPaintEngine, QImage)
|
||||
QBrush, QPen, Qt, QPointF, QTransform, QPaintEngine, QImage)
|
||||
|
||||
from calibre.ebooks.pdf.render.common import (
|
||||
Name, Array, fmtnum, Stream, Dictionary)
|
||||
from calibre.ebooks.pdf.render.serialize import Path
|
||||
from calibre.ebooks.pdf.render.gradients import LinearGradientPattern
|
||||
|
||||
def convert_path(path): # {{{
|
||||
p = Path()
|
||||
@ -248,7 +249,7 @@ class TexturePattern(TilingPattern):
|
||||
class GraphicsState(object):
|
||||
|
||||
FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin',
|
||||
'clip', 'do_fill', 'do_stroke')
|
||||
'clip_updated', 'do_fill', 'do_stroke')
|
||||
|
||||
def __init__(self):
|
||||
self.fill = QBrush()
|
||||
@ -256,7 +257,7 @@ class GraphicsState(object):
|
||||
self.opacity = 1.0
|
||||
self.transform = QTransform()
|
||||
self.brush_origin = QPointF()
|
||||
self.clip = QPainterPath()
|
||||
self.clip_updated = False
|
||||
self.do_fill = False
|
||||
self.do_stroke = True
|
||||
self.qt_pattern_cache = {}
|
||||
@ -274,16 +275,17 @@ class GraphicsState(object):
|
||||
ans.opacity = self.opacity
|
||||
ans.transform = self.transform * QTransform()
|
||||
ans.brush_origin = QPointF(self.brush_origin)
|
||||
ans.clip = self.clip
|
||||
ans.clip_updated = self.clip_updated
|
||||
ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke
|
||||
return ans
|
||||
|
||||
class Graphics(object):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, page_width_px, page_height_px):
|
||||
self.base_state = GraphicsState()
|
||||
self.current_state = GraphicsState()
|
||||
self.pending_state = None
|
||||
self.page_width_px, self.page_height_px = (page_width_px, page_height_px)
|
||||
|
||||
def begin(self, pdf):
|
||||
self.pdf = pdf
|
||||
@ -311,7 +313,7 @@ class Graphics(object):
|
||||
s.opacity = state.opacity()
|
||||
|
||||
if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
|
||||
s.clip = painter.clipPath()
|
||||
s.clip_updated = True
|
||||
|
||||
def reset(self):
|
||||
self.current_state = GraphicsState()
|
||||
@ -326,7 +328,7 @@ class Graphics(object):
|
||||
ps = self.pending_state
|
||||
pdf = self.pdf
|
||||
|
||||
if (ps.transform != pdf_state.transform or ps.clip != pdf_state.clip):
|
||||
if ps.transform != pdf_state.transform or ps.clip_updated:
|
||||
pdf.restore_stack()
|
||||
pdf.save_stack()
|
||||
pdf_state = self.base_state
|
||||
@ -341,11 +343,14 @@ class Graphics(object):
|
||||
pdf_state.brush_origin != ps.brush_origin):
|
||||
self.apply_fill(ps, pdf_system, painter)
|
||||
|
||||
if (pdf_state.clip != ps.clip):
|
||||
p = convert_path(ps.clip)
|
||||
fill_rule = {Qt.OddEvenFill:'evenodd',
|
||||
Qt.WindingFill:'winding'}[ps.clip.fillRule()]
|
||||
pdf.add_clip(p, fill_rule=fill_rule)
|
||||
if ps.clip_updated:
|
||||
ps.clip_updated = False
|
||||
path = painter.clipPath()
|
||||
if not path.isEmpty():
|
||||
p = convert_path(path)
|
||||
fill_rule = {Qt.OddEvenFill:'evenodd',
|
||||
Qt.WindingFill:'winding'}[path.fillRule()]
|
||||
pdf.add_clip(p, fill_rule=fill_rule)
|
||||
|
||||
self.current_state = self.pending_state
|
||||
self.pending_state = None
|
||||
@ -357,7 +362,7 @@ class Graphics(object):
|
||||
pdf = self.pdf
|
||||
|
||||
pattern = color = pat = None
|
||||
opacity = 1.0
|
||||
opacity = global_opacity
|
||||
do_fill = True
|
||||
|
||||
matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y())
|
||||
@ -366,29 +371,30 @@ class Graphics(object):
|
||||
self.brushobj = None
|
||||
|
||||
if style <= Qt.DiagCrossPattern:
|
||||
opacity = global_opacity * vals[-1]
|
||||
opacity *= vals[-1]
|
||||
color = vals[:3]
|
||||
|
||||
if style > Qt.SolidPattern:
|
||||
pat = QtPattern(style, matrix)
|
||||
pattern = pdf.add_pattern(pat)
|
||||
|
||||
if opacity < 1e-4 or style == Qt.NoBrush:
|
||||
do_fill = False
|
||||
|
||||
elif style == Qt.TexturePattern:
|
||||
pat = TexturePattern(brush.texture(), matrix, pdf)
|
||||
opacity = global_opacity
|
||||
if pat.paint_type == 2:
|
||||
opacity *= vals[-1]
|
||||
color = vals[:3]
|
||||
pattern = pdf.add_pattern(pat)
|
||||
|
||||
if opacity < 1e-4 or style == Qt.NoBrush:
|
||||
do_fill = False
|
||||
elif style == Qt.LinearGradientPattern:
|
||||
pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px,
|
||||
self.page_height_px)
|
||||
opacity *= pat.const_opacity
|
||||
# TODO: Add support for radial/conical gradient fills
|
||||
|
||||
if opacity < 1e-4 or style == Qt.NoBrush:
|
||||
do_fill = False
|
||||
self.brushobj = Brush(brush_origin, pat, color)
|
||||
# TODO: Add support for gradient fills
|
||||
|
||||
if pat is not None:
|
||||
pattern = pdf.add_pattern(pat)
|
||||
return color, opacity, pattern, do_fill
|
||||
|
||||
def apply_stroke(self, state, pdf_system, painter):
|
||||
@ -450,7 +456,10 @@ class Graphics(object):
|
||||
TexturePatterns and it also uses TexturePatterns to emulate gradients,
|
||||
leading to brokenness. So this method allows the paint engine to update
|
||||
the brush origin before painting an object. While not perfect, this is
|
||||
better than nothing.
|
||||
better than nothing. The problem is that if the rect being filled has a
|
||||
border, then QtWebKit generates an image of the rect size - border but
|
||||
fills the full rect, and there's no way for the paint engine to know
|
||||
that and adjust the brush origin.
|
||||
'''
|
||||
if not hasattr(self, 'last_fill') or not self.current_state.do_fill:
|
||||
return
|
||||
|
@ -17,10 +17,14 @@ from calibre.ebooks.pdf.render.common import Array, Name, Dictionary, String
|
||||
class Destination(Array):
|
||||
|
||||
def __init__(self, start_page, pos, get_pageref):
|
||||
super(Destination, self).__init__(
|
||||
[get_pageref(start_page + pos['column']), Name('XYZ'), pos['left'],
|
||||
pos['top'], None]
|
||||
)
|
||||
pnum = start_page + pos['column']
|
||||
try:
|
||||
pref = get_pageref(pnum)
|
||||
except IndexError:
|
||||
pref = get_pageref(pnum-1)
|
||||
super(Destination, self).__init__([
|
||||
pref, Name('XYZ'), pos['left'], pos['top'], None
|
||||
])
|
||||
|
||||
class Links(object):
|
||||
|
||||
@ -58,7 +62,13 @@ class Links(object):
|
||||
0])})
|
||||
if is_local:
|
||||
path = combined_path if href else path
|
||||
annot['Dest'] = self.anchors[path][frag]
|
||||
try:
|
||||
annot['Dest'] = self.anchors[path][frag]
|
||||
except KeyError:
|
||||
try:
|
||||
annot['Dest'] = self.anchors[path][None]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
url = href + (('#'+frag) if frag else '')
|
||||
purl = urlparse(url)
|
||||
|
@ -18,6 +18,7 @@ from calibre.ebooks.pdf.render.common import (
|
||||
fmtnum)
|
||||
from calibre.ebooks.pdf.render.fonts import FontManager
|
||||
from calibre.ebooks.pdf.render.links import Links
|
||||
from calibre.utils.date import utcnow
|
||||
|
||||
PDFVER = b'%PDF-1.3'
|
||||
|
||||
@ -259,12 +260,15 @@ class PDFStream(object):
|
||||
self.objects.add(PageTree(page_size))
|
||||
self.objects.add(Catalog(self.page_tree))
|
||||
self.current_page = Page(self.page_tree, compress=self.compress)
|
||||
self.info = Dictionary({'Creator':String(creator),
|
||||
'Producer':String(creator)})
|
||||
self.info = Dictionary({
|
||||
'Creator':String(creator),
|
||||
'Producer':String(creator),
|
||||
'CreationDate': utcnow(),
|
||||
})
|
||||
self.stroke_opacities, self.fill_opacities = {}, {}
|
||||
self.font_manager = FontManager(self.objects, self.compress)
|
||||
self.image_cache = {}
|
||||
self.pattern_cache = {}
|
||||
self.pattern_cache, self.shader_cache = {}, {}
|
||||
self.debug = debug
|
||||
self.links = Links(self, mark_links, page_size)
|
||||
i = QImage(1, 1, QImage.Format_ARGB32)
|
||||
@ -447,6 +451,11 @@ class PDFStream(object):
|
||||
self.pattern_cache[pattern.cache_key] = self.objects.add(pattern)
|
||||
return self.current_page.add_pattern(self.pattern_cache[pattern.cache_key])
|
||||
|
||||
def add_shader(self, shader):
|
||||
if shader.cache_key not in self.shader_cache:
|
||||
self.shader_cache[shader.cache_key] = self.objects.add(shader)
|
||||
return self.shader_cache[shader.cache_key]
|
||||
|
||||
def draw_image(self, x, y, width, height, imgref):
|
||||
name = self.current_page.add_image(imgref)
|
||||
self.current_page.write('q %s 0 0 %s %s %s cm '%(fmtnum(width),
|
||||
|
@ -83,13 +83,15 @@ def run(dev, func):
|
||||
raise SystemExit(1)
|
||||
|
||||
def brush(p, xmax, ymax):
|
||||
x = xmax/3
|
||||
x = 0
|
||||
y = 0
|
||||
w = xmax/2
|
||||
pix = QPixmap(I('console.png'))
|
||||
p.fillRect(x, y, w, w, QBrush(pix))
|
||||
|
||||
p.fillRect(0, y+xmax/1.9, w, w, QBrush(pix))
|
||||
g = QLinearGradient(QPointF(x, y), QPointF(x, y+w))
|
||||
g.setColorAt(0, QColor('#f00'))
|
||||
g.setColorAt(0.5, QColor('#fff'))
|
||||
g.setColorAt(1, QColor('#00f'))
|
||||
p.fillRect(x, y, w, w, QBrush(g))
|
||||
p.drawRect(x, y, w, w)
|
||||
|
||||
def pen(p, xmax, ymax):
|
||||
pix = QPixmap(I('console.png'))
|
||||
|
@ -766,6 +766,26 @@ class Translator(QTranslator):
|
||||
gui_thread = None
|
||||
|
||||
qt_app = None
|
||||
|
||||
def load_builtin_fonts():
|
||||
global _rating_font
|
||||
# Load the builtin fonts and any fonts added to calibre by the user to
|
||||
# Qt
|
||||
for ff in glob.glob(P('fonts/liberation/*.?tf')) + \
|
||||
[P('fonts/calibreSymbols.otf')] + \
|
||||
glob.glob(os.path.join(config_dir, 'fonts', '*.?tf')):
|
||||
if ff.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
|
||||
with open(ff, 'rb') as s:
|
||||
# Windows requires font files to be executable for them to be
|
||||
# loaded successfully, so we use the in memory loader
|
||||
fid = QFontDatabase.addApplicationFontFromData(s.read())
|
||||
if fid > -1:
|
||||
fam = QFontDatabase.applicationFontFamilies(fid)
|
||||
fam = set(map(unicode, fam))
|
||||
if u'calibre Symbols' in fam:
|
||||
_rating_font = u'calibre Symbols'
|
||||
|
||||
|
||||
class Application(QApplication):
|
||||
|
||||
def __init__(self, args, force_calibre_style=False,
|
||||
@ -798,27 +818,12 @@ class Application(QApplication):
|
||||
return ret
|
||||
|
||||
def load_builtin_fonts(self, scan_for_fonts=False):
|
||||
global _rating_font
|
||||
if scan_for_fonts:
|
||||
from calibre.utils.fonts.scanner import font_scanner
|
||||
# Start scanning the users computer for fonts
|
||||
font_scanner
|
||||
|
||||
# Load the builtin fonts and any fonts added to calibre by the user to
|
||||
# Qt
|
||||
for ff in glob.glob(P('fonts/liberation/*.?tf')) + \
|
||||
[P('fonts/calibreSymbols.otf')] + \
|
||||
glob.glob(os.path.join(config_dir, 'fonts', '*.?tf')):
|
||||
if ff.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
|
||||
with open(ff, 'rb') as s:
|
||||
# Windows requires font files to be executable for them to be
|
||||
# loaded successfully, so we use the in memory loader
|
||||
fid = QFontDatabase.addApplicationFontFromData(s.read())
|
||||
if fid > -1:
|
||||
fam = QFontDatabase.applicationFontFamilies(fid)
|
||||
fam = set(map(unicode, fam))
|
||||
if u'calibre Symbols' in fam:
|
||||
_rating_font = u'calibre Symbols'
|
||||
load_builtin_fonts()
|
||||
|
||||
def load_calibre_style(self):
|
||||
# On OS X QtCurve resets the palette, so we preserve it explicitly
|
||||
|
@ -169,6 +169,10 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
|
||||
self.choose_menu = self.qaction.menu()
|
||||
|
||||
ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
|
||||
None, None), attr='action_pick_random')
|
||||
ac.triggered.connect(self.pick_random)
|
||||
|
||||
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
||||
self.choose_menu.addAction(self.action_choose)
|
||||
|
||||
@ -176,13 +180,11 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
|
||||
self.rename_menu = QMenu(_('Rename library'))
|
||||
self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
|
||||
self.choose_menu.addAction(ac)
|
||||
self.delete_menu = QMenu(_('Remove library'))
|
||||
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
||||
|
||||
ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
|
||||
None, None), attr='action_pick_random')
|
||||
ac.triggered.connect(self.pick_random)
|
||||
self.choose_menu.addAction(ac)
|
||||
else:
|
||||
self.choose_menu.addAction(ac)
|
||||
|
||||
self.rename_separator = self.choose_menu.addSeparator()
|
||||
|
||||
|
@ -8,10 +8,10 @@ from functools import partial
|
||||
from PyQt4.Qt import QThread, QObject, Qt, QProgressDialog, pyqtSignal, QTimer
|
||||
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.gui2 import (question_dialog, error_dialog, info_dialog, gprefs,
|
||||
from calibre.gui2 import (error_dialog, info_dialog, gprefs,
|
||||
warning_dialog, available_width)
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
from calibre.ebooks.metadata import MetaInformation, authors_to_string
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.constants import preferred_encoding, filesystem_encoding, DEBUG
|
||||
from calibre.utils.config import prefs
|
||||
from calibre import prints, force_unicode, as_unicode
|
||||
@ -391,25 +391,10 @@ class Adder(QObject): # {{{
|
||||
if not duplicates:
|
||||
return self.duplicates_processed()
|
||||
self.pd.hide()
|
||||
duplicate_message = []
|
||||
for x in duplicates:
|
||||
duplicate_message.append(_('Already in calibre:'))
|
||||
matching_books = self.db.books_with_same_title(x[0])
|
||||
for book_id in matching_books:
|
||||
aut = [a.replace('|', ',') for a in (self.db.authors(book_id,
|
||||
index_is_id=True) or '').split(',')]
|
||||
duplicate_message.append('\t'+ _('%(title)s by %(author)s')%
|
||||
dict(title=self.db.title(book_id, index_is_id=True),
|
||||
author=authors_to_string(aut)))
|
||||
duplicate_message.append(_('You are trying to add:'))
|
||||
duplicate_message.append('\t'+_('%(title)s by %(author)s')%
|
||||
dict(title=x[0].title,
|
||||
author=x[0].format_field('authors')[1]))
|
||||
duplicate_message.append('')
|
||||
if question_dialog(self._parent, _('Duplicates found!'),
|
||||
_('Books with the same title as the following already '
|
||||
'exist in calibre. Add them anyway?'),
|
||||
'\n'.join(duplicate_message)):
|
||||
from calibre.gui2.dialogs.duplicates import DuplicatesQuestion
|
||||
d = DuplicatesQuestion(self.db, duplicates, self._parent)
|
||||
duplicates = tuple(d.duplicates)
|
||||
if duplicates:
|
||||
pd = QProgressDialog(_('Adding duplicates...'), '', 0, len(duplicates),
|
||||
self._parent)
|
||||
pd.setCancelButton(None)
|
||||
|
@ -369,6 +369,7 @@ class Series(Base):
|
||||
w.setMinimumContentsLength(25)
|
||||
self.name_widget = w
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||
w.editTextChanged.connect(self.series_changed)
|
||||
|
||||
self.widgets.append(QLabel('&'+self.col_metadata['name']+_(' index:'), parent))
|
||||
w = QDoubleSpinBox(parent)
|
||||
@ -389,26 +390,33 @@ class Series(Base):
|
||||
self.initial_index = s_index
|
||||
self.initial_val = val
|
||||
val = self.normalize_db_val(val)
|
||||
self.name_widget.blockSignals(True)
|
||||
self.name_widget.update_items_cache(values)
|
||||
self.name_widget.show_initial_value(val)
|
||||
self.name_widget.blockSignals(False)
|
||||
|
||||
def getter(self):
|
||||
n = unicode(self.name_widget.currentText()).strip()
|
||||
i = self.idx_widget.value()
|
||||
return n, i
|
||||
|
||||
def series_changed(self, val):
|
||||
val, s_index = self.gui_val
|
||||
if tweaks['series_index_auto_increment'] == 'no_change':
|
||||
pass
|
||||
elif tweaks['series_index_auto_increment'] == 'const':
|
||||
s_index = 1.0
|
||||
else:
|
||||
s_index = self.db.get_next_cc_series_num_for(val,
|
||||
num=self.col_id)
|
||||
self.idx_widget.setValue(s_index)
|
||||
|
||||
def commit(self, book_id, notify=False):
|
||||
val, s_index = self.gui_val
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val or s_index != self.initial_index:
|
||||
if val == '':
|
||||
val = s_index = None
|
||||
elif s_index == 0.0:
|
||||
if tweaks['series_index_auto_increment'] != 'const':
|
||||
s_index = self.db.get_next_cc_series_num_for(val,
|
||||
num=self.col_id)
|
||||
else:
|
||||
s_index = None
|
||||
return self.db.set_custom(book_id, val, extra=s_index, num=self.col_id,
|
||||
notify=notify, commit=False, allow_case_change=True)
|
||||
else:
|
||||
|
118
src/calibre/gui2/dialogs/duplicates.py
Normal file
118
src/calibre/gui2/dialogs/duplicates.py
Normal file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (QDialog, QGridLayout, QIcon, QLabel, QTreeWidget,
|
||||
QTreeWidgetItem, Qt, QFont, QDialogButtonBox)
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
|
||||
class DuplicatesQuestion(QDialog):
|
||||
|
||||
def __init__(self, db, duplicates, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.l = l = QGridLayout()
|
||||
self.setLayout(l)
|
||||
self.setWindowTitle(_('Duplicates found!'))
|
||||
self.i = i = QIcon(I('dialog_question.png'))
|
||||
self.setWindowIcon(i)
|
||||
|
||||
self.l1 = l1 = QLabel()
|
||||
self.l2 = l2 = QLabel(_(
|
||||
'Books with the same titles as the following already '
|
||||
'exist in calibre. Select which books you want added anyway.'))
|
||||
l2.setWordWrap(True)
|
||||
l1.setPixmap(i.pixmap(128, 128))
|
||||
l.addWidget(l1, 0, 0)
|
||||
l.addWidget(l2, 0, 1)
|
||||
|
||||
self.dup_list = dl = QTreeWidget(self)
|
||||
l.addWidget(dl, 1, 0, 1, 2)
|
||||
dl.setHeaderHidden(True)
|
||||
dl.addTopLevelItems(list(self.process_duplicates(db, duplicates)))
|
||||
dl.expandAll()
|
||||
dl.setIndentation(30)
|
||||
|
||||
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
l.addWidget(bb, 2, 0, 1, 2)
|
||||
self.ab = ab = bb.addButton(_('Select &all'), bb.ActionRole)
|
||||
ab.clicked.connect(self.select_all)
|
||||
self.nb = ab = bb.addButton(_('Select &none'), bb.ActionRole)
|
||||
ab.clicked.connect(self.select_none)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
self.exec_()
|
||||
|
||||
def select_all(self):
|
||||
for i in xrange(self.dup_list.topLevelItemCount()):
|
||||
x = self.dup_list.topLevelItem(i)
|
||||
x.setCheckState(0, Qt.Checked)
|
||||
|
||||
def select_none(self):
|
||||
for i in xrange(self.dup_list.topLevelItemCount()):
|
||||
x = self.dup_list.topLevelItem(i)
|
||||
x.setCheckState(0, Qt.Unchecked)
|
||||
|
||||
def reject(self):
|
||||
self.select_none()
|
||||
QDialog.reject(self)
|
||||
|
||||
def process_duplicates(self, db, duplicates):
|
||||
ta = _('%(title)s by %(author)s')
|
||||
bf = QFont(self.dup_list.font())
|
||||
bf.setBold(True)
|
||||
itf = QFont(self.dup_list.font())
|
||||
itf.setItalic(True)
|
||||
|
||||
for mi, cover, formats in duplicates:
|
||||
item = QTreeWidgetItem([ta%dict(
|
||||
title=mi.title, author=mi.format_field('authors')[1])] , 0)
|
||||
item.setCheckState(0, Qt.Checked)
|
||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable)
|
||||
item.setData(0, Qt.FontRole, bf)
|
||||
item.setData(0, Qt.UserRole, (mi, cover, formats))
|
||||
matching_books = db.books_with_same_title(mi)
|
||||
|
||||
def add_child(text):
|
||||
c = QTreeWidgetItem([text], 1)
|
||||
c.setFlags(Qt.ItemIsEnabled)
|
||||
item.addChild(c)
|
||||
return c
|
||||
|
||||
add_child(_('Already in calibre:')).setData(0, Qt.FontRole, itf)
|
||||
|
||||
for book_id in matching_books:
|
||||
aut = [a.replace('|', ',') for a in (db.authors(book_id,
|
||||
index_is_id=True) or '').split(',')]
|
||||
add_child(ta%dict(
|
||||
title=db.title(book_id, index_is_id=True),
|
||||
author=authors_to_string(aut)))
|
||||
add_child('')
|
||||
|
||||
yield item
|
||||
|
||||
@property
|
||||
def duplicates(self):
|
||||
for i in xrange(self.dup_list.topLevelItemCount()):
|
||||
x = self.dup_list.topLevelItem(i)
|
||||
if x.checkState(0) == Qt.Checked:
|
||||
yield x.data(0, Qt.UserRole).toPyObject()
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
from calibre.ebooks.metadata.book.base import Metadata as M
|
||||
from calibre.library import db
|
||||
|
||||
app = QApplication([])
|
||||
db = db()
|
||||
d = DuplicatesQuestion(db, [(M('Life of Pi', ['Yann Martel']), None, None),
|
||||
(M('Heirs of the blade', ['Adrian Tchaikovsky']), None, None)])
|
||||
print (tuple(d.duplicates))
|
||||
|
@ -1109,8 +1109,8 @@ not multiple and the destination field is multiple</string>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>205</width>
|
||||
<height>66</height>
|
||||
<width>934</width>
|
||||
<height>213</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="testgrid">
|
||||
@ -1269,8 +1269,8 @@ not multiple and the destination field is multiple</string>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>252</x>
|
||||
<y>382</y>
|
||||
<x>258</x>
|
||||
<y>638</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
@ -1285,8 +1285,8 @@ not multiple and the destination field is multiple</string>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>320</x>
|
||||
<y>382</y>
|
||||
<x>326</x>
|
||||
<y>638</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
@ -1294,5 +1294,37 @@ not multiple and the destination field is multiple</string>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>remove_all_tags</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>remove_tags</receiver>
|
||||
<slot>setDisabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>888</x>
|
||||
<y>266</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>814</x>
|
||||
<y>268</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>clear_languages</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>languages</receiver>
|
||||
<slot>setDisabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>874</x>
|
||||
<y>418</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>817</x>
|
||||
<y>420</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
@ -201,6 +201,7 @@ class SearchBar(QWidget): # {{{
|
||||
x.setObjectName("search")
|
||||
x.setToolTip(_("<p>Search the list of books by title, author, publisher, "
|
||||
"tags, comments, etc.<br><br>Words separated by spaces are ANDed"))
|
||||
x.setMinimumContentsLength(10)
|
||||
l.addWidget(x)
|
||||
|
||||
self.search_button = QToolButton()
|
||||
@ -225,7 +226,7 @@ class SearchBar(QWidget): # {{{
|
||||
|
||||
x = parent.saved_search = SavedSearchBox(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
x.setMinimumContentsLength(15)
|
||||
x.setMinimumContentsLength(10)
|
||||
x.setObjectName("saved_search")
|
||||
l.addWidget(x)
|
||||
|
||||
|
@ -1106,6 +1106,7 @@ class SortKeyGenerator(object):
|
||||
self.library_order = tweaks['title_series_sorting'] == 'library_order'
|
||||
self.data = data
|
||||
self.string_sort_key = sort_key
|
||||
self.lang_idx = field_metadata['languages']['rec_index']
|
||||
|
||||
def __call__(self, record):
|
||||
values = tuple(self.itervals(self.data[record]))
|
||||
@ -1159,7 +1160,12 @@ class SortKeyGenerator(object):
|
||||
val = ('', 1)
|
||||
else:
|
||||
if self.library_order:
|
||||
val = title_sort(val)
|
||||
try:
|
||||
lang = record[self.lang_idx].partition(u',')[0]
|
||||
except (AttributeError, ValueError, KeyError,
|
||||
IndexError, TypeError):
|
||||
lang = None
|
||||
val = title_sort(val, order='library_order', lang=lang)
|
||||
sidx_fm = self.field_metadata[name + '_index']
|
||||
sidx = record[sidx_fm['rec_index']]
|
||||
val = (self.string_sort_key(val), sidx)
|
||||
|
@ -532,6 +532,8 @@ class CustomColumns(object):
|
||||
|
||||
if data['datatype'] == 'series' and extra is None:
|
||||
(val, extra) = self._get_series_values(val)
|
||||
if extra is None:
|
||||
extra = 1.0
|
||||
|
||||
books_to_refresh = set([])
|
||||
if data['normalized']:
|
||||
|
@ -236,7 +236,7 @@ class ContentServer(object):
|
||||
newmi = mi.deepcopy_metadata()
|
||||
newmi.template_to_attribute(mi, cpb)
|
||||
|
||||
if format in ('MOBI', 'EPUB'):
|
||||
if format in {'MOBI', 'EPUB', 'AZW3'}:
|
||||
# Write the updated file
|
||||
from calibre.ebooks.metadata.meta import set_metadata
|
||||
set_metadata(fmt, newmi, format.lower())
|
||||
|
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