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
e0b4f3173d
@ -14,6 +14,7 @@ resources/scripts.pickle
|
|||||||
resources/ebook-convert-complete.pickle
|
resources/ebook-convert-complete.pickle
|
||||||
resources/builtin_recipes.xml
|
resources/builtin_recipes.xml
|
||||||
resources/builtin_recipes.zip
|
resources/builtin_recipes.zip
|
||||||
|
resources/template-functions.json
|
||||||
setup/installer/windows/calibre/build.log
|
setup/installer/windows/calibre/build.log
|
||||||
src/calibre/translations/.errors
|
src/calibre/translations/.errors
|
||||||
src/cssutils/.svn/
|
src/cssutils/.svn/
|
||||||
|
@ -19,6 +19,67 @@
|
|||||||
# new recipes:
|
# new recipes:
|
||||||
# - title:
|
# - title:
|
||||||
|
|
||||||
|
- version: 0.8.5
|
||||||
|
date: 2011-06-10
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "A new 'portable' calibre build, useful if you like to carry around calibre and its library on a USB key"
|
||||||
|
type: major
|
||||||
|
description: "For details, see: http://calibre-ebook.com/download_portable"
|
||||||
|
|
||||||
|
- title: "E-book viewer: Remember the last used font size multiplier."
|
||||||
|
tickets: [774343]
|
||||||
|
|
||||||
|
- title: "Preliminary support for the Kobo Touch. Drivers for the ZTE v9 tablet, Samsung S2, Notion Ink Adam and PocketBook 360+"
|
||||||
|
|
||||||
|
- title: "When downloading metadata merge rather than replace tags"
|
||||||
|
|
||||||
|
- title: "Edit metadata dialog: When pasting in an ISBN, if not valid ISBN if present on the clipboard popup a box for the user to enter the ISBN"
|
||||||
|
|
||||||
|
- title: "Windows build: Add code to load .pyd python extensions from a zip file. This allows many more files in the calibre installation to be zipped up, speeding up the installer."
|
||||||
|
- title: "Add an action to remove all formats from the selected books to the remove books button"
|
||||||
|
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "Various minor bug fixes to the column coloring code"
|
||||||
|
|
||||||
|
- title: "Fix the not() template function"
|
||||||
|
|
||||||
|
- title: "Nook Color/TSR: When sending books to the storage card place them in the My Files/Books subdirectory. Also do not upload cover thumbnails as users report that the NC/TSR don't use them."
|
||||||
|
tickets: [792842]
|
||||||
|
|
||||||
|
- title: "Get Books: Update plugins for Amazon and B&N stores to handle website changes. Enable some stores by default on first run. Add Zixo store"
|
||||||
|
tickets: [792762]
|
||||||
|
|
||||||
|
- title: "Comic Input: Replace the # character in filenames as it can cause problem with conversion/vieweing."
|
||||||
|
tickets: [792723]
|
||||||
|
|
||||||
|
- title: "When writing files to zipfile, reset timestamp if it doesn't fit in 1980's vintage storage structures"
|
||||||
|
|
||||||
|
- title: "Amazon metadata plugin: Fix parsing of published date from amazon.de when it has februar in it"
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Ambito
|
||||||
|
- GoComics
|
||||||
|
- Le Monde Diplomatique
|
||||||
|
- Max Planck
|
||||||
|
- express.de
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: Ambito Financiero
|
||||||
|
author: Darko Miletic
|
||||||
|
|
||||||
|
- title: Stiin Tas Technica
|
||||||
|
author: Silviu Cotoara
|
||||||
|
|
||||||
|
- title: "Metro News NL"
|
||||||
|
author: DrMerry
|
||||||
|
|
||||||
|
- title: "Brigitte.de, Polizeipresse DE and Heise Online"
|
||||||
|
author: schuster
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- version: 0.8.4
|
- version: 0.8.4
|
||||||
date: 2011-06-03
|
date: 2011-06-03
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008-2009, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
ambito.com
|
ambito.com
|
||||||
'''
|
'''
|
||||||
@ -11,51 +9,56 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class Ambito(BasicNewsRecipe):
|
class Ambito(BasicNewsRecipe):
|
||||||
title = 'Ambito.com'
|
title = 'Ambito.com'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Informacion Libre las 24 horas'
|
description = 'Ambito.com con noticias del Diario Ambito Financiero de Buenos Aires'
|
||||||
publisher = 'Ambito.com'
|
publisher = 'Editorial Nefir S.A.'
|
||||||
category = 'news, politics, Argentina'
|
category = 'news, politics, economy, finances, Argentina'
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 100
|
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'iso-8859-1'
|
encoding = 'cp1252'
|
||||||
cover_url = 'http://www.ambito.com/img/logo_.jpg'
|
masthead_url = 'http://www.ambito.com/img/logo_.jpg'
|
||||||
remove_javascript = True
|
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
|
language = 'es_AR'
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: "Trebuchet MS",Verdana,sans-serif}
|
||||||
|
.volanta{font-size: small}
|
||||||
|
.t2_portada{font-size: xx-large; font-family: Georgia,serif; color: #026698}
|
||||||
|
"""
|
||||||
|
|
||||||
html2lrf_options = [
|
|
||||||
'--comment', description
|
|
||||||
, '--category', category
|
|
||||||
, '--publisher', publisher
|
|
||||||
]
|
|
||||||
|
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'align':'justify'})]
|
keep_only_tags = [dict(name='div', attrs={'align':'justify'})]
|
||||||
|
remove_tags = [dict(name=['object','link','embed','iframe','meta','link','table','img'])]
|
||||||
remove_tags = [dict(name=['object','link'])]
|
remove_attributes = ['align']
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Principales Noticias', u'http://www.ambito.com/rss/noticiasp.asp' )
|
(u'Principales Noticias', u'http://www.ambito.com/rss/noticiasp.asp' )
|
||||||
,(u'Economia' , u'http://www.ambito.com/rss/noticias.asp?S=Econom%EDa' )
|
,(u'Economia' , u'http://www.ambito.com/rss/noticias.asp?S=Econom%EDa' )
|
||||||
,(u'Politica' , u'http://www.ambito.com/rss/noticias.asp?S=Pol%EDtica' )
|
,(u'Politica' , u'http://www.ambito.com/rss/noticias.asp?S=Pol%EDtica' )
|
||||||
,(u'Informacion General' , u'http://www.ambito.com/rss/noticias.asp?S=Informaci%F3n%20General')
|
,(u'Informacion General' , u'http://www.ambito.com/rss/noticias.asp?S=Informaci%F3n%20General')
|
||||||
,(u'Agro' , u'http://www.ambito.com/rss/noticias.asp?S=Agro' )
|
,(u'Campo' , u'http://www.ambito.com/rss/noticias.asp?S=Agro' )
|
||||||
,(u'Internacionales' , u'http://www.ambito.com/rss/noticias.asp?S=Internacionales' )
|
,(u'Internacionales' , u'http://www.ambito.com/rss/noticias.asp?S=Internacionales' )
|
||||||
,(u'Deportes' , u'http://www.ambito.com/rss/noticias.asp?S=Deportes' )
|
,(u'Deportes' , u'http://www.ambito.com/rss/noticias.asp?S=Deportes' )
|
||||||
,(u'Espectaculos' , u'http://www.ambito.com/rss/noticias.asp?S=Espect%E1culos' )
|
,(u'Espectaculos' , u'http://www.ambito.com/rss/noticias.asp?S=Espect%E1culos' )
|
||||||
,(u'Tecnologia' , u'http://www.ambito.com/rss/noticias.asp?S=Tecnologia' )
|
,(u'Tecnologia' , u'http://www.ambito.com/rss/noticias.asp?S=Tecnolog%EDa' )
|
||||||
,(u'Salud' , u'http://www.ambito.com/rss/noticias.asp?S=Salud' )
|
|
||||||
,(u'Ambito Nacional' , u'http://www.ambito.com/rss/noticias.asp?S=Ambito%20Nacional' )
|
,(u'Ambito Nacional' , u'http://www.ambito.com/rss/noticias.asp?S=Ambito%20Nacional' )
|
||||||
]
|
]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url.replace('http://www.ambito.com/noticia.asp?','http://www.ambito.com/noticias/imprimir.asp?')
|
return url.replace('/noticia.asp?','/noticias/imprimir.asp?')
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
mtag = '<meta http-equiv="Content-Language" content="es-AR"/>'
|
|
||||||
soup.head.insert(0,mtag)
|
|
||||||
for item in soup.findAll(style=True):
|
for item in soup.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
str = item.string
|
||||||
|
if str is None:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
language = 'es_AR'
|
|
||||||
|
87
recipes/ambito_financiero.recipe
Normal file
87
recipes/ambito_financiero.recipe
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
ambito.com/diario
|
||||||
|
'''
|
||||||
|
|
||||||
|
import time
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Ambito_Financiero(BasicNewsRecipe):
|
||||||
|
title = 'Ambito Financiero'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Informacion Libre las 24 horas'
|
||||||
|
publisher = 'Editorial Nefir S.A.'
|
||||||
|
category = 'news, politics, economy, Argentina'
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'cp1252'
|
||||||
|
masthead_url = 'http://www.ambito.com/diario/img/logo_af.gif'
|
||||||
|
publication_type = 'newspaper'
|
||||||
|
needs_subscription = 'optional'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'es_AR'
|
||||||
|
PREFIX = 'http://www.ambito.com'
|
||||||
|
INDEX = PREFIX + '/diario/index.asp'
|
||||||
|
LOGIN = PREFIX + '/diario/login/entrada.asp'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: "Trebuchet MS",Verdana,sans-serif}
|
||||||
|
.volanta{font-size: small}
|
||||||
|
.t2_portada{font-size: xx-large; font-family: Georgia,serif; color: #026698}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'align':'justify'})]
|
||||||
|
remove_tags = [dict(name=['object','link','embed','iframe','meta','link','table','img'])]
|
||||||
|
remove_attributes = ['align']
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
br.open(self.INDEX)
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
br.open(self.LOGIN)
|
||||||
|
br.select_form(name='frmlogin')
|
||||||
|
br['USER_NAME'] = self.username
|
||||||
|
br['USER_PASS'] = self.password
|
||||||
|
br.submit()
|
||||||
|
return br
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url.replace('/diario/noticia.asp?','/noticias/imprimir.asp?')
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
str = item.string
|
||||||
|
if str is None:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
cover_item = soup.find('img',attrs={'class':'fotodespliegue'})
|
||||||
|
if cover_item:
|
||||||
|
self.cover_url = self.PREFIX + cover_item['src']
|
||||||
|
articles = []
|
||||||
|
checker = []
|
||||||
|
for feed_link in soup.findAll('a', attrs={'class':['t0_portada','t2_portada','bajada']}):
|
||||||
|
url = self.PREFIX + feed_link['href']
|
||||||
|
title = self.tag_to_string(feed_link)
|
||||||
|
date = strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime())
|
||||||
|
if url not in checker:
|
||||||
|
checker.append(url)
|
||||||
|
articles.append({
|
||||||
|
'title' :title
|
||||||
|
,'date' :date
|
||||||
|
,'url' :url
|
||||||
|
,'description':u''
|
||||||
|
})
|
||||||
|
return [(self.title, articles)]
|
52
recipes/daily_mirror.recipe
Normal file
52
recipes/daily_mirror.recipe
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1306061239(BasicNewsRecipe):
|
||||||
|
title = u'The Daily Mirror'
|
||||||
|
description = 'News as provide by The Daily Mirror -UK'
|
||||||
|
|
||||||
|
__author__ = 'Dave Asbury'
|
||||||
|
language = 'en_GB'
|
||||||
|
|
||||||
|
cover_url = 'http://yookeo.com/screens/m/i/mirror.co.uk.jpg'
|
||||||
|
|
||||||
|
masthead_url = 'http://www.nmauk.co.uk/nma/images/daily_mirror.gif'
|
||||||
|
|
||||||
|
|
||||||
|
oldest_article = 1
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_empty_feeds = True
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1'),
|
||||||
|
dict(attrs={'class':['article-attr']}),
|
||||||
|
dict(name='div', attrs={'class' : [ 'article-body', 'crosshead']})
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class' : ['caption', 'article-resize']}),
|
||||||
|
dict( attrs={'class':'append-html'})
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
|
||||||
|
(u'News', u'http://www.mirror.co.uk/news/rss.xml')
|
||||||
|
,(u'Tech News', u'http://www.mirror.co.uk/news/technology/rss.xml')
|
||||||
|
,(u'Weird World','http://www.mirror.co.uk/news/weird-world/rss.xml')
|
||||||
|
,(u'Film Gossip','http://www.mirror.co.uk/celebs/film/rss.xml')
|
||||||
|
,(u'Music News','http://www.mirror.co.uk/celebs/music/rss.xml')
|
||||||
|
,(u'Celebs and Tv Gossip','http://www.mirror.co.uk/celebs/tv/rss.xml')
|
||||||
|
,(u'Sport','http://www.mirror.co.uk/sport/rss.xml')
|
||||||
|
,(u'Life Style','http://www.mirror.co.uk/life-style/rss.xml')
|
||||||
|
,(u'Advice','http://www.mirror.co.uk/advice/rss.xml')
|
||||||
|
,(u'Travel','http://www.mirror.co.uk/advice/travel/rss.xml')
|
||||||
|
|
||||||
|
# example of commented out feed not needed ,(u'Travel','http://www.mirror.co.uk/advice/travel/rss.xml')
|
||||||
|
]
|
||||||
|
|
@ -11,8 +11,8 @@ import mechanize, re
|
|||||||
class GoComics(BasicNewsRecipe):
|
class GoComics(BasicNewsRecipe):
|
||||||
title = 'GoComics'
|
title = 'GoComics'
|
||||||
__author__ = 'Starson17'
|
__author__ = 'Starson17'
|
||||||
__version__ = '1.05'
|
__version__ = '1.06'
|
||||||
__date__ = '19 may 2011'
|
__date__ = '07 June 2011'
|
||||||
description = u'200+ Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.'
|
description = u'200+ Comics - Customize for more days/comics: Defaults to 7 days, 25 comics - 20 general, 5 editorial.'
|
||||||
category = 'news, comics'
|
category = 'news, comics'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
@ -56,225 +56,318 @@ class GoComics(BasicNewsRecipe):
|
|||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
feeds = []
|
feeds = []
|
||||||
for title, url in [
|
for title, url in [
|
||||||
######## COMICS - GENERAL ########
|
(u"2 Cows and a Chicken", u"http://www.gocomics.com/2cowsandachicken"),
|
||||||
(u"2 Cows and a Chicken", u"http://www.gocomics.com/2cowsandachicken"),
|
#(u"9 Chickweed Lane", u"http://www.gocomics.com/9chickweedlane"),
|
||||||
# (u"9 to 5", u"http://www.gocomics.com/9to5"),
|
(u"9 to 5", u"http://www.gocomics.com/9to5"),
|
||||||
# (u"The Academia Waltz", u"http://www.gocomics.com/academiawaltz"),
|
#(u"Adam At Home", u"http://www.gocomics.com/adamathome"),
|
||||||
# (u"Adam@Home", u"http://www.gocomics.com/adamathome"),
|
(u"Agnes", u"http://www.gocomics.com/agnes"),
|
||||||
# (u"Agnes", u"http://www.gocomics.com/agnes"),
|
#(u"Alley Oop", u"http://www.gocomics.com/alleyoop"),
|
||||||
# (u"Andy Capp", u"http://www.gocomics.com/andycapp"),
|
#(u"Andy Capp", u"http://www.gocomics.com/andycapp"),
|
||||||
# (u"Animal Crackers", u"http://www.gocomics.com/animalcrackers"),
|
#(u"Animal Crackers", u"http://www.gocomics.com/animalcrackers"),
|
||||||
# (u"Annie", u"http://www.gocomics.com/annie"),
|
#(u"Annie", u"http://www.gocomics.com/annie"),
|
||||||
(u"The Argyle Sweater", u"http://www.gocomics.com/theargylesweater"),
|
#(u"Arlo & Janis", u"http://www.gocomics.com/arloandjanis"),
|
||||||
# (u"Ask Shagg", u"http://www.gocomics.com/askshagg"),
|
#(u"Ask Shagg", u"http://www.gocomics.com/askshagg"),
|
||||||
(u"B.C.", u"http://www.gocomics.com/bc"),
|
(u"B.C.", u"http://www.gocomics.com/bc"),
|
||||||
# (u"Back in the Day", u"http://www.gocomics.com/backintheday"),
|
#(u"Back in the Day", u"http://www.gocomics.com/backintheday"),
|
||||||
# (u"Bad Reporter", u"http://www.gocomics.com/badreporter"),
|
#(u"Bad Reporter", u"http://www.gocomics.com/badreporter"),
|
||||||
# (u"Baldo", u"http://www.gocomics.com/baldo"),
|
#(u"Baldo", u"http://www.gocomics.com/baldo"),
|
||||||
# (u"Ballard Street", u"http://www.gocomics.com/ballardstreet"),
|
#(u"Ballard Street", u"http://www.gocomics.com/ballardstreet"),
|
||||||
# (u"Barkeater Lake", u"http://www.gocomics.com/barkeaterlake"),
|
#(u"Barkeater Lake", u"http://www.gocomics.com/barkeaterlake"),
|
||||||
# (u"The Barn", u"http://www.gocomics.com/thebarn"),
|
#(u"Basic Instructions", u"http://www.gocomics.com/basicinstructions"),
|
||||||
# (u"Basic Instructions", u"http://www.gocomics.com/basicinstructions"),
|
#(u"Ben", u"http://www.gocomics.com/ben"),
|
||||||
# (u"Bewley", u"http://www.gocomics.com/bewley"),
|
#(u"Betty", u"http://www.gocomics.com/betty"),
|
||||||
# (u"Big Top", u"http://www.gocomics.com/bigtop"),
|
#(u"Bewley", u"http://www.gocomics.com/bewley"),
|
||||||
# (u"Biographic", u"http://www.gocomics.com/biographic"),
|
#(u"Big Nate", u"http://www.gocomics.com/bignate"),
|
||||||
(u"Birdbrains", u"http://www.gocomics.com/birdbrains"),
|
#(u"Big Top", u"http://www.gocomics.com/bigtop"),
|
||||||
# (u"Bleeker: The Rechargeable Dog", u"http://www.gocomics.com/bleeker"),
|
#(u"Biographic", u"http://www.gocomics.com/biographic"),
|
||||||
# (u"Bliss", u"http://www.gocomics.com/bliss"),
|
#(u"Birdbrains", u"http://www.gocomics.com/birdbrains"),
|
||||||
(u"Bloom County", u"http://www.gocomics.com/bloomcounty"),
|
#(u"Bleeker: The Rechargeable Dog", u"http://www.gocomics.com/bleeker"),
|
||||||
# (u"Bo Nanas", u"http://www.gocomics.com/bonanas"),
|
#(u"Bliss", u"http://www.gocomics.com/bliss"),
|
||||||
# (u"Bob the Squirrel", u"http://www.gocomics.com/bobthesquirrel"),
|
(u"Bloom County", u"http://www.gocomics.com/bloomcounty"),
|
||||||
# (u"The Boiling Point", u"http://www.gocomics.com/theboilingpoint"),
|
#(u"Bo Nanas", u"http://www.gocomics.com/bonanas"),
|
||||||
# (u"Boomerangs", u"http://www.gocomics.com/boomerangs"),
|
#(u"Bob the Squirrel", u"http://www.gocomics.com/bobthesquirrel"),
|
||||||
# (u"The Boondocks", u"http://www.gocomics.com/boondocks"),
|
#(u"Boomerangs", u"http://www.gocomics.com/boomerangs"),
|
||||||
# (u"Bottomliners", u"http://www.gocomics.com/bottomliners"),
|
#(u"Bottomliners", u"http://www.gocomics.com/bottomliners"),
|
||||||
# (u"Bound and Gagged", u"http://www.gocomics.com/boundandgagged"),
|
#(u"Bound and Gagged", u"http://www.gocomics.com/boundandgagged"),
|
||||||
# (u"Brainwaves", u"http://www.gocomics.com/brainwaves"),
|
#(u"Brainwaves", u"http://www.gocomics.com/brainwaves"),
|
||||||
# (u"Brenda Starr", u"http://www.gocomics.com/brendastarr"),
|
#(u"Brenda Starr", u"http://www.gocomics.com/brendastarr"),
|
||||||
# (u"Brewster Rockit", u"http://www.gocomics.com/brewsterrockit"),
|
#(u"Brevity", u"http://www.gocomics.com/brevity"),
|
||||||
# (u"Broom Hilda", u"http://www.gocomics.com/broomhilda"),
|
#(u"Brewster Rockit", u"http://www.gocomics.com/brewsterrockit"),
|
||||||
(u"Calvin and Hobbes", u"http://www.gocomics.com/calvinandhobbes"),
|
#(u"Broom Hilda", u"http://www.gocomics.com/broomhilda"),
|
||||||
# (u"Candorville", u"http://www.gocomics.com/candorville"),
|
(u"Calvin and Hobbes", u"http://www.gocomics.com/calvinandhobbes"),
|
||||||
# (u"Cathy", u"http://www.gocomics.com/cathy"),
|
#(u"Candorville", u"http://www.gocomics.com/candorville"),
|
||||||
# (u"C'est la Vie", u"http://www.gocomics.com/cestlavie"),
|
#(u"Cathy", u"http://www.gocomics.com/cathy"),
|
||||||
# (u"Chuckle Bros", u"http://www.gocomics.com/chucklebros"),
|
#(u"C'est la Vie", u"http://www.gocomics.com/cestlavie"),
|
||||||
# (u"Citizen Dog", u"http://www.gocomics.com/citizendog"),
|
#(u"Cheap Thrills", u"http://www.gocomics.com/cheapthrills"),
|
||||||
# (u"The City", u"http://www.gocomics.com/thecity"),
|
#(u"Chuckle Bros", u"http://www.gocomics.com/chucklebros"),
|
||||||
# (u"Cleats", u"http://www.gocomics.com/cleats"),
|
#(u"Citizen Dog", u"http://www.gocomics.com/citizendog"),
|
||||||
# (u"Close to Home", u"http://www.gocomics.com/closetohome"),
|
#(u"Cleats", u"http://www.gocomics.com/cleats"),
|
||||||
# (u"Compu-toon", u"http://www.gocomics.com/compu-toon"),
|
#(u"Close to Home", u"http://www.gocomics.com/closetohome"),
|
||||||
# (u"Cornered", u"http://www.gocomics.com/cornered"),
|
#(u"Committed", u"http://www.gocomics.com/committed"),
|
||||||
(u"Cul de Sac", u"http://www.gocomics.com/culdesac"),
|
#(u"Compu-toon", u"http://www.gocomics.com/compu-toon"),
|
||||||
# (u"Daddy's Home", u"http://www.gocomics.com/daddyshome"),
|
#(u"Cornered", u"http://www.gocomics.com/cornered"),
|
||||||
# (u"Deep Cover", u"http://www.gocomics.com/deepcover"),
|
#(u"Cow & Boy", u"http://www.gocomics.com/cow&boy"),
|
||||||
# (u"Dick Tracy", u"http://www.gocomics.com/dicktracy"),
|
#(u"Cul de Sac", u"http://www.gocomics.com/culdesac"),
|
||||||
# (u"The Dinette Set", u"http://www.gocomics.com/dinetteset"),
|
#(u"Daddy's Home", u"http://www.gocomics.com/daddyshome"),
|
||||||
# (u"Dog Eat Doug", u"http://www.gocomics.com/dogeatdoug"),
|
#(u"Deep Cover", u"http://www.gocomics.com/deepcover"),
|
||||||
# (u"Domestic Abuse", u"http://www.gocomics.com/domesticabuse"),
|
#(u"Dick Tracy", u"http://www.gocomics.com/dicktracy"),
|
||||||
# (u"Doodles", u"http://www.gocomics.com/doodles"),
|
(u"Dog Eat Doug", u"http://www.gocomics.com/dogeatdoug"),
|
||||||
(u"Doonesbury", u"http://www.gocomics.com/doonesbury"),
|
#(u"Domestic Abuse", u"http://www.gocomics.com/domesticabuse"),
|
||||||
# (u"The Doozies", u"http://www.gocomics.com/thedoozies"),
|
(u"Doodles", u"http://www.gocomics.com/doodles"),
|
||||||
# (u"The Duplex", u"http://www.gocomics.com/duplex"),
|
(u"Doonesbury", u"http://www.gocomics.com/doonesbury"),
|
||||||
# (u"Eek!", u"http://www.gocomics.com/eek"),
|
#(u"Drabble", u"http://www.gocomics.com/drabble"),
|
||||||
# (u"The Elderberries", u"http://www.gocomics.com/theelderberries"),
|
#(u"Eek!", u"http://www.gocomics.com/eek"),
|
||||||
# (u"Flight Deck", u"http://www.gocomics.com/flightdeck"),
|
#(u"F Minus", u"http://www.gocomics.com/fminus"),
|
||||||
# (u"Flo and Friends", u"http://www.gocomics.com/floandfriends"),
|
#(u"Family Tree", u"http://www.gocomics.com/familytree"),
|
||||||
# (u"The Flying McCoys", u"http://www.gocomics.com/theflyingmccoys"),
|
#(u"Farcus", u"http://www.gocomics.com/farcus"),
|
||||||
(u"For Better or For Worse", u"http://www.gocomics.com/forbetterorforworse"),
|
(u"Fat Cats Classics", u"http://www.gocomics.com/fatcatsclassics"),
|
||||||
# (u"For Heaven's Sake", u"http://www.gocomics.com/forheavenssake"),
|
#(u"Ferd'nand", u"http://www.gocomics.com/ferdnand"),
|
||||||
# (u"Fort Knox", u"http://www.gocomics.com/fortknox"),
|
#(u"Flight Deck", u"http://www.gocomics.com/flightdeck"),
|
||||||
# (u"FoxTrot", u"http://www.gocomics.com/foxtrot"),
|
(u"Flo and Friends", u"http://www.gocomics.com/floandfriends"),
|
||||||
(u"FoxTrot Classics", u"http://www.gocomics.com/foxtrotclassics"),
|
#(u"For Better or For Worse", u"http://www.gocomics.com/forbetterorforworse"),
|
||||||
# (u"Frank & Ernest", u"http://www.gocomics.com/frankandernest"),
|
#(u"For Heaven's Sake", u"http://www.gocomics.com/forheavenssake"),
|
||||||
# (u"Fred Basset", u"http://www.gocomics.com/fredbasset"),
|
#(u"Fort Knox", u"http://www.gocomics.com/fortknox"),
|
||||||
# (u"Free Range", u"http://www.gocomics.com/freerange"),
|
#(u"FoxTrot Classics", u"http://www.gocomics.com/foxtrotclassics"),
|
||||||
# (u"Frog Applause", u"http://www.gocomics.com/frogapplause"),
|
(u"FoxTrot", u"http://www.gocomics.com/foxtrot"),
|
||||||
# (u"The Fusco Brothers", u"http://www.gocomics.com/thefuscobrothers"),
|
#(u"Frank & Ernest", u"http://www.gocomics.com/frankandernest"),
|
||||||
(u"Garfield", u"http://www.gocomics.com/garfield"),
|
#(u"Frazz", u"http://www.gocomics.com/frazz"),
|
||||||
# (u"Garfield Minus Garfield", u"http://www.gocomics.com/garfieldminusgarfield"),
|
#(u"Fred Basset", u"http://www.gocomics.com/fredbasset"),
|
||||||
# (u"Gasoline Alley", u"http://www.gocomics.com/gasolinealley"),
|
#(u"Free Range", u"http://www.gocomics.com/freerange"),
|
||||||
# (u"Gil Thorp", u"http://www.gocomics.com/gilthorp"),
|
#(u"Frog Applause", u"http://www.gocomics.com/frogapplause"),
|
||||||
# (u"Ginger Meggs", u"http://www.gocomics.com/gingermeggs"),
|
#(u"Garfield Minus Garfield", u"http://www.gocomics.com/garfieldminusgarfield"),
|
||||||
# (u"Girls & Sports", u"http://www.gocomics.com/girlsandsports"),
|
(u"Garfield", u"http://www.gocomics.com/garfield"),
|
||||||
# (u"Haiku Ewe", u"http://www.gocomics.com/haikuewe"),
|
#(u"Gasoline Alley", u"http://www.gocomics.com/gasolinealley"),
|
||||||
# (u"Heart of the City", u"http://www.gocomics.com/heartofthecity"),
|
#(u"Geech Classics", u"http://www.gocomics.com/geechclassics"),
|
||||||
# (u"Heathcliff", u"http://www.gocomics.com/heathcliff"),
|
#(u"Get Fuzzy", u"http://www.gocomics.com/getfuzzy"),
|
||||||
# (u"Herb and Jamaal", u"http://www.gocomics.com/herbandjamaal"),
|
#(u"Gil Thorp", u"http://www.gocomics.com/gilthorp"),
|
||||||
# (u"Home and Away", u"http://www.gocomics.com/homeandaway"),
|
#(u"Ginger Meggs", u"http://www.gocomics.com/gingermeggs"),
|
||||||
# (u"Housebroken", u"http://www.gocomics.com/housebroken"),
|
#(u"Girls & Sports", u"http://www.gocomics.com/girlsandsports"),
|
||||||
# (u"Hubert and Abby", u"http://www.gocomics.com/hubertandabby"),
|
#(u"Graffiti", u"http://www.gocomics.com/graffiti"),
|
||||||
# (u"Imagine This", u"http://www.gocomics.com/imaginethis"),
|
#(u"Grand Avenue", u"http://www.gocomics.com/grandavenue"),
|
||||||
# (u"In the Bleachers", u"http://www.gocomics.com/inthebleachers"),
|
#(u"Haiku Ewe", u"http://www.gocomics.com/haikuewe"),
|
||||||
# (u"In the Sticks", u"http://www.gocomics.com/inthesticks"),
|
#(u"Heart of the City", u"http://www.gocomics.com/heartofthecity"),
|
||||||
# (u"Ink Pen", u"http://www.gocomics.com/inkpen"),
|
(u"Heathcliff", u"http://www.gocomics.com/heathcliff"),
|
||||||
# (u"It's All About You", u"http://www.gocomics.com/itsallaboutyou"),
|
#(u"Herb and Jamaal", u"http://www.gocomics.com/herbandjamaal"),
|
||||||
# (u"Joe Vanilla", u"http://www.gocomics.com/joevanilla"),
|
#(u"Herman", u"http://www.gocomics.com/herman"),
|
||||||
# (u"La Cucaracha", u"http://www.gocomics.com/lacucaracha"),
|
#(u"Home and Away", u"http://www.gocomics.com/homeandaway"),
|
||||||
# (u"Last Kiss", u"http://www.gocomics.com/lastkiss"),
|
#(u"Housebroken", u"http://www.gocomics.com/housebroken"),
|
||||||
# (u"Legend of Bill", u"http://www.gocomics.com/legendofbill"),
|
#(u"Hubert and Abby", u"http://www.gocomics.com/hubertandabby"),
|
||||||
# (u"Liberty Meadows", u"http://www.gocomics.com/libertymeadows"),
|
#(u"Imagine This", u"http://www.gocomics.com/imaginethis"),
|
||||||
(u"Lio", u"http://www.gocomics.com/lio"),
|
#(u"In the Bleachers", u"http://www.gocomics.com/inthebleachers"),
|
||||||
# (u"Little Dog Lost", u"http://www.gocomics.com/littledoglost"),
|
#(u"In the Sticks", u"http://www.gocomics.com/inthesticks"),
|
||||||
# (u"Little Otto", u"http://www.gocomics.com/littleotto"),
|
#(u"Ink Pen", u"http://www.gocomics.com/inkpen"),
|
||||||
# (u"Loose Parts", u"http://www.gocomics.com/looseparts"),
|
#(u"It's All About You", u"http://www.gocomics.com/itsallaboutyou"),
|
||||||
# (u"Love Is...", u"http://www.gocomics.com/loveis"),
|
#(u"Jane's World", u"http://www.gocomics.com/janesworld"),
|
||||||
# (u"Maintaining", u"http://www.gocomics.com/maintaining"),
|
#(u"Joe Vanilla", u"http://www.gocomics.com/joevanilla"),
|
||||||
# (u"The Meaning of Lila", u"http://www.gocomics.com/meaningoflila"),
|
#(u"Jump Start", u"http://www.gocomics.com/jumpstart"),
|
||||||
# (u"Middle-Aged White Guy", u"http://www.gocomics.com/middleagedwhiteguy"),
|
#(u"Kit 'N' Carlyle", u"http://www.gocomics.com/kitandcarlyle"),
|
||||||
# (u"The Middletons", u"http://www.gocomics.com/themiddletons"),
|
#(u"La Cucaracha", u"http://www.gocomics.com/lacucaracha"),
|
||||||
# (u"Momma", u"http://www.gocomics.com/momma"),
|
#(u"Last Kiss", u"http://www.gocomics.com/lastkiss"),
|
||||||
# (u"Mutt & Jeff", u"http://www.gocomics.com/muttandjeff"),
|
#(u"Legend of Bill", u"http://www.gocomics.com/legendofbill"),
|
||||||
# (u"Mythtickle", u"http://www.gocomics.com/mythtickle"),
|
#(u"Liberty Meadows", u"http://www.gocomics.com/libertymeadows"),
|
||||||
# (u"Nest Heads", u"http://www.gocomics.com/nestheads"),
|
#(u"Li'l Abner Classics", u"http://www.gocomics.com/lilabnerclassics"),
|
||||||
# (u"NEUROTICA", u"http://www.gocomics.com/neurotica"),
|
#(u"Lio", u"http://www.gocomics.com/lio"),
|
||||||
(u"New Adventures of Queen Victoria", u"http://www.gocomics.com/thenewadventuresofqueenvictoria"),
|
#(u"Little Dog Lost", u"http://www.gocomics.com/littledoglost"),
|
||||||
(u"Non Sequitur", u"http://www.gocomics.com/nonsequitur"),
|
#(u"Little Otto", u"http://www.gocomics.com/littleotto"),
|
||||||
# (u"The Norm", u"http://www.gocomics.com/thenorm"),
|
#(u"Lola", u"http://www.gocomics.com/lola"),
|
||||||
# (u"On A Claire Day", u"http://www.gocomics.com/onaclaireday"),
|
#(u"Loose Parts", u"http://www.gocomics.com/looseparts"),
|
||||||
# (u"One Big Happy", u"http://www.gocomics.com/onebighappy"),
|
#(u"Love Is...", u"http://www.gocomics.com/loveis"),
|
||||||
# (u"The Other Coast", u"http://www.gocomics.com/theothercoast"),
|
#(u"Luann", u"http://www.gocomics.com/luann"),
|
||||||
# (u"Out of the Gene Pool Re-Runs", u"http://www.gocomics.com/outofthegenepool"),
|
#(u"Maintaining", u"http://www.gocomics.com/maintaining"),
|
||||||
# (u"Overboard", u"http://www.gocomics.com/overboard"),
|
(u"Marmaduke", u"http://www.gocomics.com/marmaduke"),
|
||||||
# (u"Pibgorn", u"http://www.gocomics.com/pibgorn"),
|
#(u"Meg! Classics", u"http://www.gocomics.com/megclassics"),
|
||||||
# (u"Pibgorn Sketches", u"http://www.gocomics.com/pibgornsketches"),
|
#(u"Middle-Aged White Guy", u"http://www.gocomics.com/middleagedwhiteguy"),
|
||||||
(u"Pickles", u"http://www.gocomics.com/pickles"),
|
#(u"Minimum Security", u"http://www.gocomics.com/minimumsecurity"),
|
||||||
# (u"Pinkerton", u"http://www.gocomics.com/pinkerton"),
|
#(u"Moderately Confused", u"http://www.gocomics.com/moderatelyconfused"),
|
||||||
# (u"Pluggers", u"http://www.gocomics.com/pluggers"),
|
(u"Momma", u"http://www.gocomics.com/momma"),
|
||||||
(u"Pooch Cafe", u"http://www.gocomics.com/poochcafe"),
|
#(u"Monty", u"http://www.gocomics.com/monty"),
|
||||||
# (u"PreTeena", u"http://www.gocomics.com/preteena"),
|
#(u"Motley Classics", u"http://www.gocomics.com/motleyclassics"),
|
||||||
# (u"The Quigmans", u"http://www.gocomics.com/thequigmans"),
|
(u"Mutt & Jeff", u"http://www.gocomics.com/muttandjeff"),
|
||||||
# (u"Rabbits Against Magic", u"http://www.gocomics.com/rabbitsagainstmagic"),
|
#(u"Mythtickle", u"http://www.gocomics.com/mythtickle"),
|
||||||
(u"Real Life Adventures", u"http://www.gocomics.com/reallifeadventures"),
|
#(u"Nancy", u"http://www.gocomics.com/nancy"),
|
||||||
# (u"Red and Rover", u"http://www.gocomics.com/redandrover"),
|
#(u"Natural Selection", u"http://www.gocomics.com/naturalselection"),
|
||||||
# (u"Red Meat", u"http://www.gocomics.com/redmeat"),
|
#(u"Nest Heads", u"http://www.gocomics.com/nestheads"),
|
||||||
# (u"Reynolds Unwrapped", u"http://www.gocomics.com/reynoldsunwrapped"),
|
#(u"NEUROTICA", u"http://www.gocomics.com/neurotica"),
|
||||||
# (u"Ronaldinho Gaucho", u"http://www.gocomics.com/ronaldinhogaucho"),
|
#(u"New Adventures of Queen Victoria", u"http://www.gocomics.com/thenewadventuresofqueenvictoria"),
|
||||||
# (u"Rubes", u"http://www.gocomics.com/rubes"),
|
#(u"Non Sequitur", u"http://www.gocomics.com/nonsequitur"),
|
||||||
# (u"Scary Gary", u"http://www.gocomics.com/scarygary"),
|
#(u"Off The Mark", u"http://www.gocomics.com/offthemark"),
|
||||||
(u"Shoe", u"http://www.gocomics.com/shoe"),
|
#(u"On A Claire Day", u"http://www.gocomics.com/onaclaireday"),
|
||||||
# (u"Shoecabbage", u"http://www.gocomics.com/shoecabbage"),
|
#(u"One Big Happy Classics", u"http://www.gocomics.com/onebighappyclassics"),
|
||||||
# (u"Skin Horse", u"http://www.gocomics.com/skinhorse"),
|
#(u"One Big Happy", u"http://www.gocomics.com/onebighappy"),
|
||||||
# (u"Slowpoke", u"http://www.gocomics.com/slowpoke"),
|
#(u"Out of the Gene Pool Re-Runs", u"http://www.gocomics.com/outofthegenepool"),
|
||||||
# (u"Speed Bump", u"http://www.gocomics.com/speedbump"),
|
#(u"Over the Hedge", u"http://www.gocomics.com/overthehedge"),
|
||||||
# (u"State of the Union", u"http://www.gocomics.com/stateoftheunion"),
|
#(u"Overboard", u"http://www.gocomics.com/overboard"),
|
||||||
(u"Stone Soup", u"http://www.gocomics.com/stonesoup"),
|
#(u"PC and Pixel", u"http://www.gocomics.com/pcandpixel"),
|
||||||
# (u"Strange Brew", u"http://www.gocomics.com/strangebrew"),
|
(u"Peanuts", u"http://www.gocomics.com/peanuts"),
|
||||||
# (u"Sylvia", u"http://www.gocomics.com/sylvia"),
|
#(u"Pearls Before Swine", u"http://www.gocomics.com/pearlsbeforeswine"),
|
||||||
# (u"Tank McNamara", u"http://www.gocomics.com/tankmcnamara"),
|
#(u"Pibgorn Sketches", u"http://www.gocomics.com/pibgornsketches"),
|
||||||
# (u"Tiny Sepuku", u"http://www.gocomics.com/tinysepuku"),
|
#(u"Pibgorn", u"http://www.gocomics.com/pibgorn"),
|
||||||
# (u"TOBY", u"http://www.gocomics.com/toby"),
|
(u"Pickles", u"http://www.gocomics.com/pickles"),
|
||||||
# (u"Tom the Dancing Bug", u"http://www.gocomics.com/tomthedancingbug"),
|
#(u"Pinkerton", u"http://www.gocomics.com/pinkerton"),
|
||||||
# (u"Too Much Coffee Man", u"http://www.gocomics.com/toomuchcoffeeman"),
|
#(u"Pluggers", u"http://www.gocomics.com/pluggers"),
|
||||||
# (u"W.T. Duck", u"http://www.gocomics.com/wtduck"),
|
#(u"Pooch Cafe", u"http://www.gocomics.com/poochcafe"),
|
||||||
# (u"Watch Your Head", u"http://www.gocomics.com/watchyourhead"),
|
#(u"PreTeena", u"http://www.gocomics.com/preteena"),
|
||||||
# (u"Wee Pals", u"http://www.gocomics.com/weepals"),
|
#(u"Prickly City", u"http://www.gocomics.com/pricklycity"),
|
||||||
# (u"Winnie the Pooh", u"http://www.gocomics.com/winniethepooh"),
|
#(u"Rabbits Against Magic", u"http://www.gocomics.com/rabbitsagainstmagic"),
|
||||||
(u"Wizard of Id", u"http://www.gocomics.com/wizardofid"),
|
#(u"Raising Duncan Classics", u"http://www.gocomics.com/raisingduncanclassics"),
|
||||||
# (u"Working It Out", u"http://www.gocomics.com/workingitout"),
|
#(u"Real Life Adventures", u"http://www.gocomics.com/reallifeadventures"),
|
||||||
# (u"Yenny", u"http://www.gocomics.com/yenny"),
|
#(u"Reality Check", u"http://www.gocomics.com/realitycheck"),
|
||||||
# (u"Zack Hill", u"http://www.gocomics.com/zackhill"),
|
#(u"Red and Rover", u"http://www.gocomics.com/redandrover"),
|
||||||
(u"Ziggy", u"http://www.gocomics.com/ziggy"),
|
#(u"Red Meat", u"http://www.gocomics.com/redmeat"),
|
||||||
######## COMICS - EDITORIAL ########
|
#(u"Reynolds Unwrapped", u"http://www.gocomics.com/reynoldsunwrapped"),
|
||||||
("Lalo Alcaraz","http://www.gocomics.com/laloalcaraz"),
|
#(u"Rip Haywire", u"http://www.gocomics.com/riphaywire"),
|
||||||
("Nick Anderson","http://www.gocomics.com/nickanderson"),
|
#(u"Ripley's Believe It or Not!", u"http://www.gocomics.com/ripleysbelieveitornot"),
|
||||||
("Chuck Asay","http://www.gocomics.com/chuckasay"),
|
#(u"Ronaldinho Gaucho", u"http://www.gocomics.com/ronaldinhogaucho"),
|
||||||
("Tony Auth","http://www.gocomics.com/tonyauth"),
|
#(u"Rose Is Rose", u"http://www.gocomics.com/roseisrose"),
|
||||||
("Donna Barstow","http://www.gocomics.com/donnabarstow"),
|
#(u"Rubes", u"http://www.gocomics.com/rubes"),
|
||||||
# ("Bruce Beattie","http://www.gocomics.com/brucebeattie"),
|
#(u"Rudy Park", u"http://www.gocomics.com/rudypark"),
|
||||||
# ("Clay Bennett","http://www.gocomics.com/claybennett"),
|
#(u"Scary Gary", u"http://www.gocomics.com/scarygary"),
|
||||||
# ("Lisa Benson","http://www.gocomics.com/lisabenson"),
|
#(u"Shirley and Son Classics", u"http://www.gocomics.com/shirleyandsonclassics"),
|
||||||
# ("Steve Benson","http://www.gocomics.com/stevebenson"),
|
#(u"Shoe", u"http://www.gocomics.com/shoe"),
|
||||||
# ("Chip Bok","http://www.gocomics.com/chipbok"),
|
#(u"Shoecabbage", u"http://www.gocomics.com/shoecabbage"),
|
||||||
# ("Steve Breen","http://www.gocomics.com/stevebreen"),
|
#(u"Skin Horse", u"http://www.gocomics.com/skinhorse"),
|
||||||
# ("Chris Britt","http://www.gocomics.com/chrisbritt"),
|
#(u"Slowpoke", u"http://www.gocomics.com/slowpoke"),
|
||||||
# ("Stuart Carlson","http://www.gocomics.com/stuartcarlson"),
|
#(u"Soup To Nutz", u"http://www.gocomics.com/souptonutz"),
|
||||||
# ("Ken Catalino","http://www.gocomics.com/kencatalino"),
|
#(u"Speed Bump", u"http://www.gocomics.com/speedbump"),
|
||||||
# ("Paul Conrad","http://www.gocomics.com/paulconrad"),
|
#(u"Spot The Frog", u"http://www.gocomics.com/spotthefrog"),
|
||||||
# ("Jeff Danziger","http://www.gocomics.com/jeffdanziger"),
|
#(u"State of the Union", u"http://www.gocomics.com/stateoftheunion"),
|
||||||
# ("Matt Davies","http://www.gocomics.com/mattdavies"),
|
#(u"Stone Soup", u"http://www.gocomics.com/stonesoup"),
|
||||||
# ("John Deering","http://www.gocomics.com/johndeering"),
|
#(u"Strange Brew", u"http://www.gocomics.com/strangebrew"),
|
||||||
# ("Bob Gorrell","http://www.gocomics.com/bobgorrell"),
|
#(u"Sylvia", u"http://www.gocomics.com/sylvia"),
|
||||||
# ("Walt Handelsman","http://www.gocomics.com/walthandelsman"),
|
#(u"Tank McNamara", u"http://www.gocomics.com/tankmcnamara"),
|
||||||
# ("Clay Jones","http://www.gocomics.com/clayjones"),
|
#(u"Tarzan Classics", u"http://www.gocomics.com/tarzanclassics"),
|
||||||
# ("Kevin Kallaugher","http://www.gocomics.com/kevinkallaugher"),
|
#(u"That's Life", u"http://www.gocomics.com/thatslife"),
|
||||||
# ("Steve Kelley","http://www.gocomics.com/stevekelley"),
|
#(u"The Academia Waltz", u"http://www.gocomics.com/academiawaltz"),
|
||||||
# ("Dick Locher","http://www.gocomics.com/dicklocher"),
|
#(u"The Argyle Sweater", u"http://www.gocomics.com/theargylesweater"),
|
||||||
# ("Chan Lowe","http://www.gocomics.com/chanlowe"),
|
#(u"The Barn", u"http://www.gocomics.com/thebarn"),
|
||||||
# ("Mike Luckovich","http://www.gocomics.com/mikeluckovich"),
|
#(u"The Boiling Point", u"http://www.gocomics.com/theboilingpoint"),
|
||||||
# ("Gary Markstein","http://www.gocomics.com/garymarkstein"),
|
#(u"The Boondocks", u"http://www.gocomics.com/boondocks"),
|
||||||
# ("Glenn McCoy","http://www.gocomics.com/glennmccoy"),
|
#(u"The Born Loser", u"http://www.gocomics.com/thebornloser"),
|
||||||
# ("Jim Morin","http://www.gocomics.com/jimmorin"),
|
#(u"The Buckets", u"http://www.gocomics.com/thebuckets"),
|
||||||
# ("Jack Ohman","http://www.gocomics.com/jackohman"),
|
#(u"The City", u"http://www.gocomics.com/thecity"),
|
||||||
# ("Pat Oliphant","http://www.gocomics.com/patoliphant"),
|
#(u"The Dinette Set", u"http://www.gocomics.com/dinetteset"),
|
||||||
# ("Joel Pett","http://www.gocomics.com/joelpett"),
|
#(u"The Doozies", u"http://www.gocomics.com/thedoozies"),
|
||||||
# ("Ted Rall","http://www.gocomics.com/tedrall"),
|
#(u"The Duplex", u"http://www.gocomics.com/duplex"),
|
||||||
# ("Michael Ramirez","http://www.gocomics.com/michaelramirez"),
|
#(u"The Elderberries", u"http://www.gocomics.com/theelderberries"),
|
||||||
# ("Marshall Ramsey","http://www.gocomics.com/marshallramsey"),
|
#(u"The Flying McCoys", u"http://www.gocomics.com/theflyingmccoys"),
|
||||||
# ("Steve Sack","http://www.gocomics.com/stevesack"),
|
#(u"The Fusco Brothers", u"http://www.gocomics.com/thefuscobrothers"),
|
||||||
# ("Ben Sargent","http://www.gocomics.com/bensargent"),
|
#(u"The Grizzwells", u"http://www.gocomics.com/thegrizzwells"),
|
||||||
# ("Drew Sheneman","http://www.gocomics.com/drewsheneman"),
|
#(u"The Humble Stumble", u"http://www.gocomics.com/thehumblestumble"),
|
||||||
# ("John Sherffius","http://www.gocomics.com/johnsherffius"),
|
#(u"The Knight Life", u"http://www.gocomics.com/theknightlife"),
|
||||||
# ("Small World","http://www.gocomics.com/smallworld"),
|
#(u"The Meaning of Lila", u"http://www.gocomics.com/meaningoflila"),
|
||||||
# ("Scott Stantis","http://www.gocomics.com/scottstantis"),
|
#(u"The Middletons", u"http://www.gocomics.com/themiddletons"),
|
||||||
# ("Wayne Stayskal","http://www.gocomics.com/waynestayskal"),
|
#(u"The Norm", u"http://www.gocomics.com/thenorm"),
|
||||||
# ("Dana Summers","http://www.gocomics.com/danasummers"),
|
#(u"The Other Coast", u"http://www.gocomics.com/theothercoast"),
|
||||||
# ("Paul Szep","http://www.gocomics.com/paulszep"),
|
#(u"The Quigmans", u"http://www.gocomics.com/thequigmans"),
|
||||||
# ("Mike Thompson","http://www.gocomics.com/mikethompson"),
|
#(u"The Sunshine Club", u"http://www.gocomics.com/thesunshineclub"),
|
||||||
# ("Tom Toles","http://www.gocomics.com/tomtoles"),
|
#(u"Tiny Sepuk", u"http://www.gocomics.com/tinysepuk"),
|
||||||
# ("Gary Varvel","http://www.gocomics.com/garyvarvel"),
|
#(u"TOBY", u"http://www.gocomics.com/toby"),
|
||||||
# ("ViewsAfrica","http://www.gocomics.com/viewsafrica"),
|
#(u"Tom the Dancing Bug", u"http://www.gocomics.com/tomthedancingbug"),
|
||||||
# ("ViewsAmerica","http://www.gocomics.com/viewsamerica"),
|
#(u"Too Much Coffee Man", u"http://www.gocomics.com/toomuchcoffeeman"),
|
||||||
# ("ViewsAsia","http://www.gocomics.com/viewsasia"),
|
#(u"Unstrange Phenomena", u"http://www.gocomics.com/unstrangephenomena"),
|
||||||
# ("ViewsBusiness","http://www.gocomics.com/viewsbusiness"),
|
#(u"W.T. Duck", u"http://www.gocomics.com/wtduck"),
|
||||||
# ("ViewsEurope","http://www.gocomics.com/viewseurope"),
|
#(u"Watch Your Head", u"http://www.gocomics.com/watchyourhead"),
|
||||||
# ("ViewsLatinAmerica","http://www.gocomics.com/viewslatinamerica"),
|
#(u"Wee Pals", u"http://www.gocomics.com/weepals"),
|
||||||
# ("ViewsMidEast","http://www.gocomics.com/viewsmideast"),
|
#(u"Winnie the Pooh", u"http://www.gocomics.com/winniethepooh"),
|
||||||
# ("Views of the World","http://www.gocomics.com/viewsoftheworld"),
|
#(u"Wizard of Id", u"http://www.gocomics.com/wizardofid"),
|
||||||
# ("Kerry Waghorn","http://www.gocomics.com/facesinthenews"),
|
#(u"Working Daze", u"http://www.gocomics.com/workingdaze"),
|
||||||
# ("Dan Wasserman","http://www.gocomics.com/danwasserman"),
|
#(u"Working It Out", u"http://www.gocomics.com/workingitout"),
|
||||||
# ("Signe Wilkinson","http://www.gocomics.com/signewilkinson"),
|
#(u"Yenny", u"http://www.gocomics.com/yenny"),
|
||||||
# ("Wit of the World","http://www.gocomics.com/witoftheworld"),
|
#(u"Zack Hill", u"http://www.gocomics.com/zackhill"),
|
||||||
# ("Don Wright","http://www.gocomics.com/donwright"),
|
(u"Ziggy", u"http://www.gocomics.com/ziggy"),
|
||||||
|
#
|
||||||
|
######## EDITORIAL CARTOONS #####################
|
||||||
|
(u"Adam Zyglis", u"http://www.gocomics.com/adamzyglis"),
|
||||||
|
#(u"Andy Singer", u"http://www.gocomics.com/andysinger"),
|
||||||
|
#(u"Ben Sargent",u"http://www.gocomics.com/bensargent"),
|
||||||
|
#(u"Bill Day", u"http://www.gocomics.com/billday"),
|
||||||
|
#(u"Bill Schorr", u"http://www.gocomics.com/billschorr"),
|
||||||
|
#(u"Bob Englehart", u"http://www.gocomics.com/bobenglehart"),
|
||||||
|
(u"Bob Gorrell",u"http://www.gocomics.com/bobgorrell"),
|
||||||
|
#(u"Brian Fairrington", u"http://www.gocomics.com/brianfairrington"),
|
||||||
|
#(u"Bruce Beattie", u"http://www.gocomics.com/brucebeattie"),
|
||||||
|
#(u"Cam Cardow", u"http://www.gocomics.com/camcardow"),
|
||||||
|
#(u"Chan Lowe",u"http://www.gocomics.com/chanlowe"),
|
||||||
|
#(u"Chip Bok",u"http://www.gocomics.com/chipbok"),
|
||||||
|
#(u"Chris Britt",u"http://www.gocomics.com/chrisbritt"),
|
||||||
|
#(u"Chuck Asay",u"http://www.gocomics.com/chuckasay"),
|
||||||
|
#(u"Clay Bennett",u"http://www.gocomics.com/claybennett"),
|
||||||
|
#(u"Clay Jones",u"http://www.gocomics.com/clayjones"),
|
||||||
|
#(u"Dan Wasserman",u"http://www.gocomics.com/danwasserman"),
|
||||||
|
#(u"Dana Summers",u"http://www.gocomics.com/danasummers"),
|
||||||
|
#(u"Daryl Cagle", u"http://www.gocomics.com/darylcagle"),
|
||||||
|
#(u"David Fitzsimmons", u"http://www.gocomics.com/davidfitzsimmons"),
|
||||||
|
(u"Dick Locher",u"http://www.gocomics.com/dicklocher"),
|
||||||
|
#(u"Don Wright",u"http://www.gocomics.com/donwright"),
|
||||||
|
#(u"Donna Barstow",u"http://www.gocomics.com/donnabarstow"),
|
||||||
|
#(u"Drew Litton", u"http://www.gocomics.com/drewlitton"),
|
||||||
|
#(u"Drew Sheneman",u"http://www.gocomics.com/drewsheneman"),
|
||||||
|
#(u"Ed Stein", u"http://www.gocomics.com/edstein"),
|
||||||
|
#(u"Eric Allie", u"http://www.gocomics.com/ericallie"),
|
||||||
|
#(u"Gary Markstein", u"http://www.gocomics.com/garymarkstein"),
|
||||||
|
#(u"Gary McCoy", u"http://www.gocomics.com/garymccoy"),
|
||||||
|
#(u"Gary Varvel", u"http://www.gocomics.com/garyvarvel"),
|
||||||
|
#(u"Glenn McCoy",u"http://www.gocomics.com/glennmccoy"),
|
||||||
|
#(u"Henry Payne", u"http://www.gocomics.com/henrypayne"),
|
||||||
|
#(u"Jack Ohman",u"http://www.gocomics.com/jackohman"),
|
||||||
|
#(u"JD Crowe", u"http://www.gocomics.com/jdcrowe"),
|
||||||
|
#(u"Jeff Danziger",u"http://www.gocomics.com/jeffdanziger"),
|
||||||
|
#(u"Jeff Parker", u"http://www.gocomics.com/jeffparker"),
|
||||||
|
#(u"Jeff Stahler", u"http://www.gocomics.com/jeffstahler"),
|
||||||
|
#(u"Jerry Holbert", u"http://www.gocomics.com/jerryholbert"),
|
||||||
|
#(u"Jim Morin",u"http://www.gocomics.com/jimmorin"),
|
||||||
|
#(u"Joel Pett",u"http://www.gocomics.com/joelpett"),
|
||||||
|
#(u"John Cole", u"http://www.gocomics.com/johncole"),
|
||||||
|
#(u"John Darkow", u"http://www.gocomics.com/johndarkow"),
|
||||||
|
#(u"John Deering",u"http://www.gocomics.com/johndeering"),
|
||||||
|
#(u"John Sherffius", u"http://www.gocomics.com/johnsherffius"),
|
||||||
|
#(u"Ken Catalino",u"http://www.gocomics.com/kencatalino"),
|
||||||
|
#(u"Kerry Waghorn",u"http://www.gocomics.com/facesinthenews"),
|
||||||
|
#(u"Kevin Kallaugher",u"http://www.gocomics.com/kevinkallaugher"),
|
||||||
|
#(u"Lalo Alcaraz",u"http://www.gocomics.com/laloalcaraz"),
|
||||||
|
#(u"Larry Wright", u"http://www.gocomics.com/larrywright"),
|
||||||
|
#(u"Lisa Benson", u"http://www.gocomics.com/lisabenson"),
|
||||||
|
#(u"Marshall Ramsey", u"http://www.gocomics.com/marshallramsey"),
|
||||||
|
#(u"Matt Bors", u"http://www.gocomics.com/mattbors"),
|
||||||
|
#(u"Matt Davies",u"http://www.gocomics.com/mattdavies"),
|
||||||
|
#(u"Michael Ramirez", u"http://www.gocomics.com/michaelramirez"),
|
||||||
|
#(u"Mike Keefe", u"http://www.gocomics.com/mikekeefe"),
|
||||||
|
#(u"Mike Luckovich", u"http://www.gocomics.com/mikeluckovich"),
|
||||||
|
#(u"MIke Thompson", u"http://www.gocomics.com/mikethompson"),
|
||||||
|
#(u"Monte Wolverton", u"http://www.gocomics.com/montewolverton"),
|
||||||
|
#(u"Mr. Fish", u"http://www.gocomics.com/mrfish"),
|
||||||
|
#(u"Nate Beeler", u"http://www.gocomics.com/natebeeler"),
|
||||||
|
#(u"Nick Anderson", u"http://www.gocomics.com/nickanderson"),
|
||||||
|
#(u"Pat Bagley", u"http://www.gocomics.com/patbagley"),
|
||||||
|
#(u"Pat Oliphant",u"http://www.gocomics.com/patoliphant"),
|
||||||
|
#(u"Paul Conrad",u"http://www.gocomics.com/paulconrad"),
|
||||||
|
#(u"Paul Szep", u"http://www.gocomics.com/paulszep"),
|
||||||
|
#(u"RJ Matson", u"http://www.gocomics.com/rjmatson"),
|
||||||
|
#(u"Rob Rogers", u"http://www.gocomics.com/robrogers"),
|
||||||
|
#(u"Robert Ariail", u"http://www.gocomics.com/robertariail"),
|
||||||
|
#(u"Scott Stantis", u"http://www.gocomics.com/scottstantis"),
|
||||||
|
#(u"Signe Wilkinson", u"http://www.gocomics.com/signewilkinson"),
|
||||||
|
#(u"Small World",u"http://www.gocomics.com/smallworld"),
|
||||||
|
#(u"Steve Benson", u"http://www.gocomics.com/stevebenson"),
|
||||||
|
#(u"Steve Breen", u"http://www.gocomics.com/stevebreen"),
|
||||||
|
#(u"Steve Kelley", u"http://www.gocomics.com/stevekelley"),
|
||||||
|
#(u"Steve Sack", u"http://www.gocomics.com/stevesack"),
|
||||||
|
#(u"Stuart Carlson",u"http://www.gocomics.com/stuartcarlson"),
|
||||||
|
#(u"Ted Rall",u"http://www.gocomics.com/tedrall"),
|
||||||
|
#(u"(Th)ink", u"http://www.gocomics.com/think"),
|
||||||
|
#(u"Tom Toles",u"http://www.gocomics.com/tomtoles"),
|
||||||
|
(u"Tony Auth",u"http://www.gocomics.com/tonyauth"),
|
||||||
|
#(u"Views of the World",u"http://www.gocomics.com/viewsoftheworld"),
|
||||||
|
#(u"ViewsAfrica",u"http://www.gocomics.com/viewsafrica"),
|
||||||
|
#(u"ViewsAmerica",u"http://www.gocomics.com/viewsamerica"),
|
||||||
|
#(u"ViewsAsia",u"http://www.gocomics.com/viewsasia"),
|
||||||
|
#(u"ViewsBusiness",u"http://www.gocomics.com/viewsbusiness"),
|
||||||
|
#(u"ViewsEurope",u"http://www.gocomics.com/viewseurope"),
|
||||||
|
#(u"ViewsLatinAmerica",u"http://www.gocomics.com/viewslatinamerica"),
|
||||||
|
#(u"ViewsMidEast",u"http://www.gocomics.com/viewsmideast"),
|
||||||
|
(u"Walt Handelsman",u"http://www.gocomics.com/walthandelsman"),
|
||||||
|
#(u"Wayne Stayskal",u"http://www.gocomics.com/waynestayskal"),
|
||||||
|
#(u"Wit of the World",u"http://www.gocomics.com/witoftheworld"),
|
||||||
]:
|
]:
|
||||||
print 'Working on: ', title
|
print 'Working on: ', title
|
||||||
articles = self.make_links(url)
|
articles = self.make_links(url)
|
||||||
@ -352,3 +445,4 @@ class GoComics(BasicNewsRecipe):
|
|||||||
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ class HBR(BasicNewsRecipe):
|
|||||||
title = 'Harvard Business Review Blogs'
|
title = 'Harvard Business Review Blogs'
|
||||||
description = 'To subscribe go to http://hbr.harvardbusiness.org'
|
description = 'To subscribe go to http://hbr.harvardbusiness.org'
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman, enhanced by BrianG'
|
__author__ = 'Kovid Goyal, enhanced by BrianG'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
|
BIN
recipes/icons/ambito_financiero.png
Normal file
BIN
recipes/icons/ambito_financiero.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 508 B |
BIN
recipes/icons/observatorul_cultural.png
Normal file
BIN
recipes/icons/observatorul_cultural.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
recipes/icons/stiintasitehnica.png
Normal file
BIN
recipes/icons/stiintasitehnica.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 703 B |
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
mondediplo.com
|
mondediplo.com
|
||||||
'''
|
'''
|
||||||
@ -11,7 +11,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
||||||
title = 'Le Monde diplomatique - English edition'
|
title = 'Le Monde diplomatique - English edition'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Real journalism making sense of the world around us'
|
description = "Le Monde diplomatique is the place you go when you want to know what's really happening. This is a major international paper that is truly independent, that sees the world in fresh ways, that focuses on places no other publications reach. We offer a clear, considered view of the conflicting interests and complexities of a modern global world. LMD in English is a concise version of the Paris-based parent edition, publishing all the major stories each month, expertly translated, and with some London-based commissions too. We offer a taster of LMD quality on our website where a selection of articles are available each month."
|
||||||
publisher = 'Le Monde diplomatique'
|
publisher = 'Le Monde diplomatique'
|
||||||
category = 'news, politics, world'
|
category = 'news, politics, world'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
@ -26,13 +26,19 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
|||||||
INDEX = PREFIX + strftime('%Y/%m/')
|
INDEX = PREFIX + strftime('%Y/%m/')
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'en'
|
language = 'en'
|
||||||
extra_css = ' body{font-family: "Luxi sans","Lucida sans","Lucida Grande",Lucida,"Lucida Sans Unicode",sans-serif} .surtitre{font-size: 1.2em; font-variant: small-caps; margin-bottom: 0.5em} .chapo{font-size: 1.2em; font-weight: bold; margin: 1em 0 0.5em} .texte{font-family: Georgia,"Times New Roman",serif} h1{color: #990000} .notes{border-top: 1px solid #CCCCCC; font-size: 0.9em; line-height: 1.4em} '
|
extra_css = """
|
||||||
|
body{font-family: "Luxi sans","Lucida sans","Lucida Grande",Lucida,"Lucida Sans Unicode",sans-serif}
|
||||||
|
.surtitre{font-size: 1.2em; font-variant: small-caps; margin-bottom: 0.5em}
|
||||||
|
.chapo{font-size: 1.2em; font-weight: bold; margin: 1em 0 0.5em}
|
||||||
|
.texte{font-family: Georgia,"Times New Roman",serif} h1{color: #990000}
|
||||||
|
.notes{border-top: 1px solid #CCCCCC; font-size: 0.9em; line-height: 1.4em}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
, 'tags' : category
|
, 'tags' : category
|
||||||
, 'publisher' : publisher
|
, 'publisher' : publisher
|
||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_browser(self):
|
def get_browser(self):
|
||||||
@ -51,7 +57,7 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
|||||||
, dict(name='div',attrs={'class':'notes surlignable'})
|
, dict(name='div',attrs={'class':'notes surlignable'})
|
||||||
]
|
]
|
||||||
remove_tags = [dict(name=['object','link','script','iframe','base'])]
|
remove_tags = [dict(name=['object','link','script','iframe','base'])]
|
||||||
remove_attributes = ['height','width']
|
remove_attributes = ['height','width','name','lang']
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
articles = []
|
articles = []
|
||||||
@ -75,3 +81,24 @@ class LeMondeDiplomatiqueEn(BasicNewsRecipe):
|
|||||||
})
|
})
|
||||||
return [(self.title, articles)]
|
return [(self.title, articles)]
|
||||||
|
|
||||||
|
def get_cover_url(self):
|
||||||
|
cover_url = None
|
||||||
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
cover_item = soup.find('div',attrs={'class':'current'})
|
||||||
|
if cover_item:
|
||||||
|
ap = cover_item.find('img',attrs={'class':'spip_logos'})
|
||||||
|
if ap:
|
||||||
|
cover_url = self.INDEX + ap['src']
|
||||||
|
return cover_url
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
return soup
|
||||||
|
45
recipes/metro_news_nl.recipe
Normal file
45
recipes/metro_news_nl.recipe
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1306097511(BasicNewsRecipe):
|
||||||
|
title = u'Metro Nieuws NL'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
__author__ = u'DrMerry'
|
||||||
|
description = u'Metro Nederland'
|
||||||
|
language = u'nl'
|
||||||
|
simultaneous_downloads = 5
|
||||||
|
delay = 1
|
||||||
|
# timefmt = ' [%A, %d %B, %Y]'
|
||||||
|
timefmt = ''
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
remove_empty_feeds = True
|
||||||
|
cover_url = 'http://www.readmetro.com/img/en/metroholland/last/1/small.jpg'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
publication_type = 'newspaper'
|
||||||
|
remove_tags_before = dict(name='div', attrs={'id':'date'})
|
||||||
|
remove_tags_after = dict(name='div', attrs={'id':'column-1-3'})
|
||||||
|
encoding = 'utf-8'
|
||||||
|
extra_css = '#date {font-size: 10px} .article-image-caption {font-size: 8px}'
|
||||||
|
|
||||||
|
remove_tags = [dict(name='div', attrs={'class':[ 'metroCommentFormWrap',
|
||||||
|
'commentForm', 'metroCommentInnerWrap', 'article-slideshow-counter-container', 'article-slideshow-control', 'ad', 'header-links',
|
||||||
|
'art-rgt','pluck-app pluck-comm', 'share-and-byline', 'article-tools-below-title', 'col-179 ', 'related-links', 'clear padding-top-15', 'share-tools', 'article-page-auto-pushes', 'footer-edit']}),
|
||||||
|
dict(name='div', attrs={'id':['article-2', 'article-4', 'article-1', 'navigation', 'footer', 'header', 'comments', 'sidebar']}),
|
||||||
|
dict(name='iframe')]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Binnenland', u'http://www.metronieuws.nl/rss.xml?c=1277377288-3'),
|
||||||
|
(u'Economie', u'http://www.metronieuws.nl/rss.xml?c=1278070988-0'),
|
||||||
|
(u'Den Haag', u'http://www.metronieuws.nl/rss.xml?c=1289013337-3'),
|
||||||
|
(u'Rotterdam', u'http://www.metronieuws.nl/rss.xml?c=1289013337-2'),
|
||||||
|
(u'Amsterdam', u'http://www.metronieuws.nl/rss.xml?c=1289013337-1'),
|
||||||
|
(u'Columns', u'http://www.metronieuws.nl/rss.xml?c=1277377288-17'),
|
||||||
|
(u'Entertainment', u'http://www.metronieuws.nl/rss.xml?c=1277377288-2'),
|
||||||
|
(u'Dot', u'http://www.metronieuws.nl/rss.xml?c=1283166782-12'),
|
||||||
|
(u'Familie', u'http://www.metronieuws.nl/rss.xml?c=1283166782-9'),
|
||||||
|
(u'Blogs', u'http://www.metronieuws.nl/rss.xml?c=1295586825-6'),
|
||||||
|
(u'Reizen', u'http://www.metronieuws.nl/rss.xml?c=1277377288-13'),
|
||||||
|
(u'Carrière', u'http://www.metronieuws.nl/rss.xml?c=1278070988-1'),
|
||||||
|
(u'Sport', u'http://www.metronieuws.nl/rss.xml?c=1277377288-12')
|
||||||
|
]
|
42
recipes/nme.recipe
Normal file
42
recipes/nme.recipe
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1306061239(BasicNewsRecipe):
|
||||||
|
title = u'New Musical Express Magazine'
|
||||||
|
__author__ = "scissors"
|
||||||
|
language = 'en'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
cover_url = 'http://tawanda3000.files.wordpress.com/2011/02/nme-logo.jpg'
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict( attrs={'class':'clear_icons'}),
|
||||||
|
dict( attrs={'class':'share_links'}),
|
||||||
|
dict( attrs={'id':'right_panel'}),
|
||||||
|
dict( attrs={'class':'today box'})
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
|
||||||
|
dict(name='h1'),
|
||||||
|
#dict(name='h3'),
|
||||||
|
dict(attrs={'class' : 'BText'}),
|
||||||
|
dict(attrs={'class' : 'Bmore'}),
|
||||||
|
dict(attrs={'class' : 'bPosts'}),
|
||||||
|
dict(attrs={'class' : 'text'}),
|
||||||
|
dict(attrs={'id' : 'article_gallery'}),
|
||||||
|
dict(attrs={'class' : 'article_text'})
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'NME News', u'http://feeds2.feedburner.com/nmecom/rss/newsxml'),
|
||||||
|
(u'Reviews', u'http://feeds2.feedburner.com/nme/SdML'),
|
||||||
|
(u'Blogs', u'http://www.nme.com/blog/index.php?blog=140&tempskin=_rss2'),
|
||||||
|
|
||||||
|
]
|
56
recipes/stiintasitehnica.recipe
Normal file
56
recipes/stiintasitehnica.recipe
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = u'2011, Silviu Cotoar\u0103'
|
||||||
|
'''
|
||||||
|
stiintasitehnica.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Stiintasitehnica(BasicNewsRecipe):
|
||||||
|
title = u'\u0218tiin\u021b\u0103 \u015fi Tehnic\u0103'
|
||||||
|
__author__ = u'Silviu Cotoar\u0103'
|
||||||
|
description = u'\u0218tiin\u021b\u0103 \u015fi Tehnic\u0103'
|
||||||
|
publisher = u'\u0218tiin\u021b\u0103 \u015fi Tehnic\u0103'
|
||||||
|
oldest_article = 50
|
||||||
|
language = 'ro'
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
category = u'Ziare,Reviste,Stiinta,Tehnica'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
cover_url = 'http://www.stiintasitehnica.com/images/logo.jpg'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comments' : description
|
||||||
|
,'tags' : category
|
||||||
|
,'language' : language
|
||||||
|
,'publisher' : publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':'mainColumn2'})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='span', attrs={'class':['redEar']})
|
||||||
|
, dict(name='table', attrs={'class':['connect_widget_interactive_area']})
|
||||||
|
, dict(name='div', attrs={'class':['panel-overlay']})
|
||||||
|
, dict(name='div', attrs={'id':['pointer']})
|
||||||
|
, dict(name='img', attrs={'class':['nav-next', 'nav-prev']})
|
||||||
|
, dict(name='table', attrs={'class':['connect_widget_interactive_area']})
|
||||||
|
, dict(name='hr', attrs={'class':['dotted']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='hr', attrs={'class':['dotted']})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Feeds', u'http://www.stiintasitehnica.com/rss/stiri.xml')
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
@ -20,8 +20,8 @@
|
|||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="{prefix}/static/jquery.multiselect.min.js"></script>
|
src="{prefix}/static/jquery.multiselect.min.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{prefix}/static/stacktrace.js"></script>
|
||||||
<script type="text/javascript" src="{prefix}/static/browse/browse.js"></script>
|
<script type="text/javascript" src="{prefix}/static/browse/browse.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var sort_cookie_name = "{sort_cookie_name}";
|
var sort_cookie_name = "{sort_cookie_name}";
|
||||||
|
@ -129,7 +129,13 @@ function toplevel() {
|
|||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
function render_error(msg) {
|
function render_error(msg) {
|
||||||
return '<div class="ui-widget"><div class="ui-state-error ui-corner-all" style="padding: 0pt 0.7em"><p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: 0.3em"> </span><strong>Error: </strong>'+msg+"</p></div></div>"
|
var st = "";
|
||||||
|
try {
|
||||||
|
var st = printStackTrace();
|
||||||
|
st = st.join('\n\n');
|
||||||
|
} catch(e) {
|
||||||
|
}
|
||||||
|
return '<div class="ui-widget"><div class="ui-state-error ui-corner-all" style="padding: 0pt 0.7em"><p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: 0.3em"> </span><strong>Error: </strong>'+msg+"<pre>"+st+"</pre></p></div></div>"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category feed {{{
|
// Category feed {{{
|
||||||
|
371
resources/content_server/stacktrace.js
Normal file
371
resources/content_server/stacktrace.js
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
|
||||||
|
// Luke Smith http://lucassmith.name/ (2008)
|
||||||
|
// Loic Dachary <loic@dachary.org> (2008)
|
||||||
|
// Johan Euphrosine <proppy@aminche.com> (2008)
|
||||||
|
// Oyvind Sean Kinsey http://kinsey.no/blog (2010)
|
||||||
|
// Victor Homyakov <victor-homyakov@users.sourceforge.net> (2010)
|
||||||
|
//
|
||||||
|
// Information and discussions
|
||||||
|
// http://jspoker.pokersource.info/skin/test-printstacktrace.html
|
||||||
|
// http://eriwen.com/javascript/js-stack-trace/
|
||||||
|
// http://eriwen.com/javascript/stacktrace-update/
|
||||||
|
// http://pastie.org/253058
|
||||||
|
//
|
||||||
|
// guessFunctionNameFromLines comes from firebug
|
||||||
|
//
|
||||||
|
// Software License Agreement (BSD License)
|
||||||
|
//
|
||||||
|
// Copyright (c) 2007, Parakey Inc.
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Redistribution and use of this software in source and binary forms, with or without modification,
|
||||||
|
// are permitted provided that the following conditions are met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer.
|
||||||
|
//
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the
|
||||||
|
// following disclaimer in the documentation and/or other
|
||||||
|
// materials provided with the distribution.
|
||||||
|
//
|
||||||
|
// * Neither the name of Parakey Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products
|
||||||
|
// derived from this software without specific prior
|
||||||
|
// written permission of Parakey Inc.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
||||||
|
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||||
|
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||||
|
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function giving a function stack trace with a forced or passed in Error
|
||||||
|
*
|
||||||
|
* @cfg {Error} e The error to create a stacktrace from (optional)
|
||||||
|
* @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
|
||||||
|
* @return {Array} of Strings with functions, lines, files, and arguments where possible
|
||||||
|
*/
|
||||||
|
function printStackTrace(options) {
|
||||||
|
options = options || {guess: true};
|
||||||
|
var ex = options.e || null, guess = !!options.guess;
|
||||||
|
var p = new printStackTrace.implementation(), result = p.run(ex);
|
||||||
|
return (guess) ? p.guessAnonymousFunctions(result) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
printStackTrace.implementation = function() {
|
||||||
|
};
|
||||||
|
|
||||||
|
printStackTrace.implementation.prototype = {
|
||||||
|
run: function(ex) {
|
||||||
|
ex = ex || this.createException();
|
||||||
|
// Do not use the stored mode: different exceptions in Chrome
|
||||||
|
// may or may not have arguments or stack
|
||||||
|
var mode = this.mode(ex);
|
||||||
|
// Use either the stored mode, or resolve it
|
||||||
|
//var mode = this._mode || this.mode(ex);
|
||||||
|
if (mode === 'other') {
|
||||||
|
return this.other(arguments.callee);
|
||||||
|
} else {
|
||||||
|
return this[mode](ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createException: function() {
|
||||||
|
try {
|
||||||
|
this.undef();
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {String} mode of operation for the environment in question.
|
||||||
|
*/
|
||||||
|
mode: function(e) {
|
||||||
|
if (e['arguments'] && e.stack) {
|
||||||
|
return (this._mode = 'chrome');
|
||||||
|
} else if (e.message && typeof window !== 'undefined' && window.opera) {
|
||||||
|
return (this._mode = e.stacktrace ? 'opera10' : 'opera');
|
||||||
|
} else if (e.stack) {
|
||||||
|
return (this._mode = 'firefox');
|
||||||
|
}
|
||||||
|
return (this._mode = 'other');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a context, function name, and callback function, overwrite it so that it calls
|
||||||
|
* printStackTrace() first with a callback and then runs the rest of the body.
|
||||||
|
*
|
||||||
|
* @param {Object} context of execution (e.g. window)
|
||||||
|
* @param {String} functionName to instrument
|
||||||
|
* @param {Function} function to call with a stack trace on invocation
|
||||||
|
*/
|
||||||
|
instrumentFunction: function(context, functionName, callback) {
|
||||||
|
context = context || window;
|
||||||
|
var original = context[functionName];
|
||||||
|
context[functionName] = function instrumented() {
|
||||||
|
callback.call(this, printStackTrace().slice(4));
|
||||||
|
return context[functionName]._instrumented.apply(this, arguments);
|
||||||
|
};
|
||||||
|
context[functionName]._instrumented = original;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a context and function name of a function that has been
|
||||||
|
* instrumented, revert the function to it's original (non-instrumented)
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @param {Object} context of execution (e.g. window)
|
||||||
|
* @param {String} functionName to de-instrument
|
||||||
|
*/
|
||||||
|
deinstrumentFunction: function(context, functionName) {
|
||||||
|
if (context[functionName].constructor === Function &&
|
||||||
|
context[functionName]._instrumented &&
|
||||||
|
context[functionName]._instrumented.constructor === Function) {
|
||||||
|
context[functionName] = context[functionName]._instrumented;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an Error object, return a formatted Array based on Chrome's stack string.
|
||||||
|
*
|
||||||
|
* @param e - Error object to inspect
|
||||||
|
* @return Array<String> of function calls, files and line numbers
|
||||||
|
*/
|
||||||
|
chrome: function(e) {
|
||||||
|
//return e.stack.replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n');
|
||||||
|
return e.stack.replace(/^\S[^\(]+?[\n$]/gm, '').
|
||||||
|
replace(/^\s+at\s+/gm, '').
|
||||||
|
replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2').
|
||||||
|
replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an Error object, return a formatted Array based on Firefox's stack string.
|
||||||
|
*
|
||||||
|
* @param e - Error object to inspect
|
||||||
|
* @return Array<String> of function calls, files and line numbers
|
||||||
|
*/
|
||||||
|
firefox: function(e) {
|
||||||
|
return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
|
||||||
|
*
|
||||||
|
* @param e - Error object to inspect
|
||||||
|
* @return Array<String> of function calls, files and line numbers
|
||||||
|
*/
|
||||||
|
opera10: function(e) {
|
||||||
|
var stack = e.stacktrace;
|
||||||
|
var lines = stack.split('\n'), ANON = '{anonymous}', lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
|
||||||
|
for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
|
||||||
|
if (lineRE.test(lines[i])) {
|
||||||
|
var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
|
||||||
|
var fnName = RegExp.$3;
|
||||||
|
fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON);
|
||||||
|
lines[j++] = fnName + '@' + location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.splice(j, lines.length - j);
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Opera 7.x-9.x only!
|
||||||
|
opera: function(e) {
|
||||||
|
var lines = e.message.split('\n'), ANON = '{anonymous}', lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i, i, j, len;
|
||||||
|
|
||||||
|
for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
|
||||||
|
//TODO: RegExp.exec() would probably be cleaner here
|
||||||
|
if (lineRE.test(lines[i])) {
|
||||||
|
lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.splice(j, lines.length - j);
|
||||||
|
return lines;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Safari, IE, and others
|
||||||
|
other: function(curr) {
|
||||||
|
var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10;
|
||||||
|
while (curr && stack.length < maxStackSize) {
|
||||||
|
fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
|
||||||
|
args = Array.prototype.slice.call(curr['arguments'] || []);
|
||||||
|
stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
|
||||||
|
curr = curr.caller;
|
||||||
|
}
|
||||||
|
return stack;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given arguments array as a String, subsituting type names for non-string types.
|
||||||
|
*
|
||||||
|
* @param {Arguments} object
|
||||||
|
* @return {Array} of Strings with stringified arguments
|
||||||
|
*/
|
||||||
|
stringifyArguments: function(args) {
|
||||||
|
var slice = Array.prototype.slice;
|
||||||
|
for (var i = 0; i < args.length; ++i) {
|
||||||
|
var arg = args[i];
|
||||||
|
if (arg === undefined) {
|
||||||
|
args[i] = 'undefined';
|
||||||
|
} else if (arg === null) {
|
||||||
|
args[i] = 'null';
|
||||||
|
} else if (arg.constructor) {
|
||||||
|
if (arg.constructor === Array) {
|
||||||
|
if (arg.length < 3) {
|
||||||
|
args[i] = '[' + this.stringifyArguments(arg) + ']';
|
||||||
|
} else {
|
||||||
|
args[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']';
|
||||||
|
}
|
||||||
|
} else if (arg.constructor === Object) {
|
||||||
|
args[i] = '#object';
|
||||||
|
} else if (arg.constructor === Function) {
|
||||||
|
args[i] = '#function';
|
||||||
|
} else if (arg.constructor === String) {
|
||||||
|
args[i] = '"' + arg + '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args.join(',');
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceCache: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the text from a given URL.
|
||||||
|
*/
|
||||||
|
ajax: function(url) {
|
||||||
|
var req = this.createXMLHTTPObject();
|
||||||
|
if (!req) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req.open('GET', url, false);
|
||||||
|
req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
|
||||||
|
req.send('');
|
||||||
|
return req.responseText;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try XHR methods in order and store XHR factory.
|
||||||
|
*
|
||||||
|
* @return <Function> XHR function or equivalent
|
||||||
|
*/
|
||||||
|
createXMLHTTPObject: function() {
|
||||||
|
var xmlhttp, XMLHttpFactories = [
|
||||||
|
function() {
|
||||||
|
return new XMLHttpRequest();
|
||||||
|
}, function() {
|
||||||
|
return new ActiveXObject('Msxml2.XMLHTTP');
|
||||||
|
}, function() {
|
||||||
|
return new ActiveXObject('Msxml3.XMLHTTP');
|
||||||
|
}, function() {
|
||||||
|
return new ActiveXObject('Microsoft.XMLHTTP');
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (var i = 0; i < XMLHttpFactories.length; i++) {
|
||||||
|
try {
|
||||||
|
xmlhttp = XMLHttpFactories[i]();
|
||||||
|
// Use memoization to cache the factory
|
||||||
|
this.createXMLHTTPObject = XMLHttpFactories[i];
|
||||||
|
return xmlhttp;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a URL, check if it is in the same domain (so we can get the source
|
||||||
|
* via Ajax).
|
||||||
|
*
|
||||||
|
* @param url <String> source url
|
||||||
|
* @return False if we need a cross-domain request
|
||||||
|
*/
|
||||||
|
isSameDomain: function(url) {
|
||||||
|
return url.indexOf(location.hostname) !== -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get source code from given URL if in the same domain.
|
||||||
|
*
|
||||||
|
* @param url <String> JS source URL
|
||||||
|
* @return <Array> Array of source code lines
|
||||||
|
*/
|
||||||
|
getSource: function(url) {
|
||||||
|
if (!(url in this.sourceCache)) {
|
||||||
|
this.sourceCache[url] = this.ajax(url).split('\n');
|
||||||
|
}
|
||||||
|
return this.sourceCache[url];
|
||||||
|
},
|
||||||
|
|
||||||
|
guessAnonymousFunctions: function(stack) {
|
||||||
|
for (var i = 0; i < stack.length; ++i) {
|
||||||
|
var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
|
||||||
|
var frame = stack[i], m = reStack.exec(frame);
|
||||||
|
if (m) {
|
||||||
|
var file = m[1], lineno = m[4], charno = m[7] || 0; //m[7] is character position in Chrome
|
||||||
|
if (file && this.isSameDomain(file) && lineno) {
|
||||||
|
var functionName = this.guessAnonymousFunction(file, lineno, charno);
|
||||||
|
stack[i] = frame.replace('{anonymous}', functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stack;
|
||||||
|
},
|
||||||
|
|
||||||
|
guessAnonymousFunction: function(url, lineNo, charNo) {
|
||||||
|
var ret;
|
||||||
|
try {
|
||||||
|
ret = this.findFunctionName(this.getSource(url), lineNo);
|
||||||
|
} catch (e) {
|
||||||
|
ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
findFunctionName: function(source, lineNo) {
|
||||||
|
// FIXME findFunctionName fails for compressed source
|
||||||
|
// (more than one function on the same line)
|
||||||
|
// TODO use captured args
|
||||||
|
// function {name}({args}) m[1]=name m[2]=args
|
||||||
|
var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/;
|
||||||
|
// {name} = function ({args}) TODO args capture
|
||||||
|
// /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/
|
||||||
|
var reFunctionExpression = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/;
|
||||||
|
// {name} = eval()
|
||||||
|
var reFunctionEvaluation = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/;
|
||||||
|
// Walk backwards in the source lines until we find
|
||||||
|
// the line which matches one of the patterns above
|
||||||
|
var code = "", line, maxLines = 10, m;
|
||||||
|
for (var i = 0; i < maxLines; ++i) {
|
||||||
|
// FIXME lineNo is 1-based, source[] is 0-based
|
||||||
|
line = source[lineNo - i];
|
||||||
|
if (line) {
|
||||||
|
code = line + code;
|
||||||
|
m = reFunctionExpression.exec(code);
|
||||||
|
if (m && m[1]) {
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
m = reFunctionDeclaration.exec(code);
|
||||||
|
if (m && m[1]) {
|
||||||
|
//return m[1] + "(" + (m[2] || "") + ")";
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
m = reFunctionEvaluation.exec(code);
|
||||||
|
if (m && m[1]) {
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '(?)';
|
||||||
|
}
|
||||||
|
};
|
@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n",
|
|
||||||
"contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val, flags=re.I):\n return value_if_present\n else:\n return value_if_not\n",
|
|
||||||
"divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n",
|
|
||||||
"uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n",
|
|
||||||
"strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n",
|
|
||||||
"in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n if l:\n for v in l:\n if re.search(pat, v, flags=re.I):\n return fv\n return nfv\n",
|
|
||||||
"not": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
|
|
||||||
"ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n",
|
|
||||||
"booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n",
|
|
||||||
"select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n",
|
|
||||||
"strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
|
|
||||||
"first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n",
|
|
||||||
"re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val, flags=re.I)\n",
|
|
||||||
"subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n",
|
|
||||||
"list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n",
|
|
||||||
"shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n",
|
|
||||||
"field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
|
|
||||||
"add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n",
|
|
||||||
"lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val, flags=re.I):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n",
|
|
||||||
"template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n",
|
|
||||||
"print": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n print args\n return None\n",
|
|
||||||
"merge_lists": "def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):\n l1 = [l.strip() for l in list1.split(separator) if l.strip()]\n l2 = [l.strip() for l in list2.split(separator) if l.strip()]\n lcl1 = set([icu_lower(l) for l in l1])\n res = []\n for i in l1:\n res.append(i)\n for i in l2:\n if icu_lower(i) not in lcl1:\n res.append(i)\n return ', '.join(sorted(res, key=sort_key))\n",
|
|
||||||
"str_in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n c = [v.strip() for v in str.split(sep) if v.strip()]\n if l:\n for v in l:\n for t in c:\n if strcmp(t, v) == 0:\n return fv\n return nfv\n",
|
|
||||||
"titlecase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return titlecase(val)\n",
|
|
||||||
"subitems": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n items = [v.strip() for v in val.split(',')]\n rv = set()\n for item in items:\n component = item.split('.')\n try:\n if ei == 0:\n rv.add('.'.join(component[si:]))\n else:\n rv.add('.'.join(component[si:ei]))\n except:\n pass\n return ', '.join(sorted(rv, key=sort_key))\n",
|
|
||||||
"sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n",
|
|
||||||
"test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n",
|
|
||||||
"eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n",
|
|
||||||
"multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
|
|
||||||
"format_date": "def evaluate(self, formatter, kwargs, mi, locals, val, format_string):\n if not val or val == 'None':\n return ''\n try:\n dt = parse_date(val)\n s = format_date(dt, format_string)\n except:\n s = 'BAD DATE'\n return s\n",
|
|
||||||
"capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n",
|
|
||||||
"identifier_in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, ident, fv, nfv):\n l = [v.strip() for v in val.split(',') if v.strip()]\n (id, _, regexp) = ident.partition(':')\n if not id:\n return nfv\n id += ':'\n if l:\n for v in l:\n if v.startswith(id):\n if not regexp or re.search(regexp, v[len(id):], flags=re.I):\n return fv\n return nfv\n",
|
|
||||||
"count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n",
|
|
||||||
"lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n",
|
|
||||||
"substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
|
|
||||||
"or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
|
|
||||||
"switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val, flags=re.I):\n return args[i+1]\n i += 2\n",
|
|
||||||
"ondevice": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.ondevice_col:\n return _('Yes')\n return ''\n",
|
|
||||||
"assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n",
|
|
||||||
"raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n",
|
|
||||||
"cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x and x != 'None' else 0)\n y = float(y if y and y != 'None' else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n"
|
|
||||||
}
|
|
@ -32,16 +32,11 @@
|
|||||||
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
|
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
|
||||||
</title>
|
</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
a { color : #0002CC }
|
body { text-align : justify }
|
||||||
|
|
||||||
a:hover { color : #BF0000 }
|
|
||||||
|
|
||||||
body { background-color : #FEFEFE; color : #000000; font-family : Verdana, Geneva, Arial, Helvetica, sans-serif; text-align : justify }
|
|
||||||
|
|
||||||
h1{ font-size : 160%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Black; background-color : #E7E7E7; margin-left : 0px; page-break-before : always; }
|
h1{ font-size : 160%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Black; background-color : #E7E7E7; margin-left : 0px; page-break-before : always; }
|
||||||
|
|
||||||
h2{ font-size : 130%; font-style : normal; font-weight : bold; text-align : left; background-color : #EEEEEE; border : 1px solid Gray; page-break-before : always; }
|
h2{ font-size : 130%; font-style : normal; font-weight : bold; text-align : left; background-color : #EEEEEE; border : 1px solid Gray; page-break-before : always; }
|
||||||
|
|
||||||
h3{ font-size : 110%; font-style : normal; font-weight : bold; text-align : left; background-color : #F1F1F1; border : 1px solid Silver;}
|
h3{ font-size : 110%; font-style : normal; font-weight : bold; text-align : left; background-color : #F1F1F1; border : 1px solid Silver;}
|
||||||
|
|
||||||
h4{ font-size : 100%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
h4{ font-size : 100%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
||||||
@ -56,13 +51,11 @@
|
|||||||
|
|
||||||
hr { color : Black }
|
hr { color : Black }
|
||||||
|
|
||||||
div {font-family : "Times New Roman", Times, serif; text-align : justify}
|
|
||||||
|
|
||||||
ul {margin-left: 0}
|
ul {margin-left: 0}
|
||||||
|
|
||||||
.epigraph{width:50%; margin-left : 35%;}
|
.epigraph{width:50%; margin-left : 35%;}
|
||||||
|
|
||||||
div.paragraph { text-align: justify; text-indent: 2em; }
|
div.paragraph { text-indent: 2em; }
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" type="text/css" href="inline-styles.css" />
|
<link rel="stylesheet" type="text/css" href="inline-styles.css" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -8,8 +8,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import sys, os, textwrap, subprocess, shutil, tempfile, atexit, stat, shlex
|
import sys, os, textwrap, subprocess, shutil, tempfile, atexit, stat, shlex
|
||||||
|
|
||||||
from setup import Command, islinux, isfreebsd, isbsd, basenames, modules, functions, \
|
from setup import (Command, islinux, isbsd, basenames, modules, functions,
|
||||||
__appname__, __version__
|
__appname__, __version__)
|
||||||
|
|
||||||
HEADER = '''\
|
HEADER = '''\
|
||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python2
|
||||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import os, shutil, subprocess
|
import os, shutil, subprocess
|
||||||
|
|
||||||
from setup import Command, __appname__
|
from setup import Command, __appname__, __version__
|
||||||
from setup.installer import VMInstaller
|
from setup.installer import VMInstaller
|
||||||
|
|
||||||
class Win(Command):
|
class Win(Command):
|
||||||
@ -43,4 +43,11 @@ class Win32(VMInstaller):
|
|||||||
self.warn('Failed to freeze')
|
self.warn('Failed to freeze')
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
installer = 'dist/%s-portable-%s.zip'%(__appname__, __version__)
|
||||||
|
subprocess.check_call(('scp',
|
||||||
|
'xp_build:build/%s/%s'%(__appname__, installer), 'dist'))
|
||||||
|
if not os.path.exists(installer):
|
||||||
|
self.warn('Failed to get portable installer')
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<WixLocalization Culture="en-us" xmlns="http://schemas.microsoft.com/wix/2006/localization">
|
<WixLocalization Culture="en-us" xmlns="http://schemas.microsoft.com/wix/2006/localization">
|
||||||
<String Id="AdvancedWelcomeEulaDlgDescriptionPerUser">If you are upgrading from a {app} version older than 0.6.17, please uninstall {app} first. Click Advanced to change installation settings.</String>
|
<String Id="AdvancedWelcomeEulaDlgDescriptionPerUser">Click Advanced to change installation settings.</String>
|
||||||
<String Id="ProgressTextFileCost">Computing space requirements, this may take upto five minutes...</String>
|
<String Id="ProgressTextFileCost">Computing space requirements, this may take upto five minutes...</String>
|
||||||
<String Id="ProgressTextCostInitialize">Computing space requirements, this may take upto five minutes...</String>
|
<String Id="ProgressTextCostInitialize">Computing space requirements, this may take upto five minutes...</String>
|
||||||
<String Id="ProgressTextCostFinalize">Computing space requirements, this may take upto five minutes...</String>
|
<String Id="ProgressTextCostFinalize">Computing space requirements, this may take upto five minutes...</String>
|
||||||
|
@ -8,8 +8,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import sys, os, shutil, glob, py_compile, subprocess, re, zipfile, time
|
import sys, os, shutil, glob, py_compile, subprocess, re, zipfile, time
|
||||||
|
|
||||||
from setup import Command, modules, functions, basenames, __version__, \
|
from setup import (Command, modules, functions, basenames, __version__,
|
||||||
__appname__
|
__appname__)
|
||||||
from setup.build_environment import msvc, MT, RC
|
from setup.build_environment import msvc, MT, RC
|
||||||
from setup.installer.windows.wix import WixMixIn
|
from setup.installer.windows.wix import WixMixIn
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
|
|||||||
SW = r'C:\cygwin\home\kovid\sw'
|
SW = r'C:\cygwin\home\kovid\sw'
|
||||||
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.6.6',
|
IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-6.6.6',
|
||||||
'VisualMagick', 'bin')
|
'VisualMagick', 'bin')
|
||||||
|
CRT = r'C:\Microsoft.VC90.CRT'
|
||||||
|
|
||||||
VERSION = re.sub('[a-z]\d+', '', __version__)
|
VERSION = re.sub('[a-z]\d+', '', __version__)
|
||||||
WINVER = VERSION+'.0'
|
WINVER = VERSION+'.0'
|
||||||
@ -49,7 +50,7 @@ def walk(dir):
|
|||||||
|
|
||||||
class Win32Freeze(Command, WixMixIn):
|
class Win32Freeze(Command, WixMixIn):
|
||||||
|
|
||||||
description = 'Free windows calibre installation'
|
description = 'Freeze windows calibre installation'
|
||||||
|
|
||||||
def add_options(self, parser):
|
def add_options(self, parser):
|
||||||
parser.add_option('--no-ice', default=False, action='store_true',
|
parser.add_option('--no-ice', default=False, action='store_true',
|
||||||
@ -72,7 +73,9 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
self.lib_dir = self.j(self.base, 'Lib')
|
self.lib_dir = self.j(self.base, 'Lib')
|
||||||
self.pylib = self.j(self.base, 'pylib.zip')
|
self.pylib = self.j(self.base, 'pylib.zip')
|
||||||
self.dll_dir = self.j(self.base, 'DLLs')
|
self.dll_dir = self.j(self.base, 'DLLs')
|
||||||
self.plugins_dir = os.path.join(self.base, 'plugins')
|
self.plugins_dir = os.path.join(self.base, 'plugins2')
|
||||||
|
self.portable_base = self.j(self.d(self.base), 'Calibre Portable')
|
||||||
|
self.obj_dir = self.j(self.src_root, 'build', 'launcher')
|
||||||
|
|
||||||
self.initbase()
|
self.initbase()
|
||||||
self.build_launchers()
|
self.build_launchers()
|
||||||
@ -81,7 +84,33 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
self.embed_manifests()
|
self.embed_manifests()
|
||||||
self.install_site_py()
|
self.install_site_py()
|
||||||
self.archive_lib_dir()
|
self.archive_lib_dir()
|
||||||
|
self.remove_CRT_from_manifests()
|
||||||
self.create_installer()
|
self.create_installer()
|
||||||
|
self.build_portable()
|
||||||
|
|
||||||
|
def remove_CRT_from_manifests(self):
|
||||||
|
'''
|
||||||
|
The dependency on the CRT is removed from the manifests of all DLLs.
|
||||||
|
This allows the CRT loaded by the .exe files to be used instead.
|
||||||
|
'''
|
||||||
|
search_pat = re.compile(r'(?is)<dependency>.*Microsoft\.VC\d+\.CRT')
|
||||||
|
repl_pat = re.compile(
|
||||||
|
r'(?is)<dependency>.*?Microsoft\.VC\d+\.CRT.*?</dependency>')
|
||||||
|
|
||||||
|
for dll in glob.glob(self.j(self.dll_dir, '*.dll')):
|
||||||
|
bn = self.b(dll)
|
||||||
|
with open(dll, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
match = search_pat.search(raw)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
self.info('Removing CRT dependency from manifest of: %s'%bn)
|
||||||
|
# Blank out the bytes corresponding to the dependency specification
|
||||||
|
nraw = repl_pat.sub(lambda m: b' '*len(m.group()), raw)
|
||||||
|
if len(nraw) != len(raw):
|
||||||
|
raise Exception('Something went wrong with %s'%bn)
|
||||||
|
with open(dll, 'wb') as f:
|
||||||
|
f.write(nraw)
|
||||||
|
|
||||||
def initbase(self):
|
def initbase(self):
|
||||||
if self.e(self.base):
|
if self.e(self.base):
|
||||||
@ -103,6 +132,9 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
def freeze(self):
|
def freeze(self):
|
||||||
shutil.copy2(self.j(self.src_root, 'LICENSE'), self.base)
|
shutil.copy2(self.j(self.src_root, 'LICENSE'), self.base)
|
||||||
|
|
||||||
|
self.info('Adding CRT')
|
||||||
|
shutil.copytree(CRT, self.j(self.base, os.path.basename(CRT)))
|
||||||
|
|
||||||
self.info('Adding resources...')
|
self.info('Adding resources...')
|
||||||
tgt = self.j(self.base, 'resources')
|
tgt = self.j(self.base, 'resources')
|
||||||
if os.path.exists(tgt):
|
if os.path.exists(tgt):
|
||||||
@ -147,6 +179,24 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
shutil.copytree(self.j(comext, 'shell'), self.j(sp_dir, 'win32com', 'shell'))
|
shutil.copytree(self.j(comext, 'shell'), self.j(sp_dir, 'win32com', 'shell'))
|
||||||
shutil.rmtree(comext)
|
shutil.rmtree(comext)
|
||||||
|
|
||||||
|
# Fix PyCrypto, removing the bootstrap .py modules that load the .pyd
|
||||||
|
# modules, since they do not work when in a zip file
|
||||||
|
for crypto_dir in glob.glob(self.j(sp_dir, 'pycrypto-*', 'Crypto')):
|
||||||
|
for dirpath, dirnames, filenames in os.walk(crypto_dir):
|
||||||
|
for f in filenames:
|
||||||
|
name, ext = os.path.splitext(f)
|
||||||
|
if ext == '.pyd':
|
||||||
|
with open(self.j(dirpath, name+'.py')) as f:
|
||||||
|
raw = f.read().strip()
|
||||||
|
if (not raw.startswith('def __bootstrap__') or not
|
||||||
|
raw.endswith('__bootstrap__()')):
|
||||||
|
raise Exception('The PyCrypto file %r has non'
|
||||||
|
' bootstrap code'%self.j(dirpath, f))
|
||||||
|
for ext in ('.py', '.pyc', '.pyo'):
|
||||||
|
x = self.j(dirpath, name+ext)
|
||||||
|
if os.path.exists(x):
|
||||||
|
os.remove(x)
|
||||||
|
|
||||||
for pat in (r'PyQt4\uic\port_v3', ):
|
for pat in (r'PyQt4\uic\port_v3', ):
|
||||||
x = glob.glob(self.j(self.lib_dir, 'site-packages', pat))[0]
|
x = glob.glob(self.j(self.lib_dir, 'site-packages', pat))[0]
|
||||||
shutil.rmtree(x)
|
shutil.rmtree(x)
|
||||||
@ -197,6 +247,10 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
if os.path.exists(tg):
|
if os.path.exists(tg):
|
||||||
shutil.rmtree(tg)
|
shutil.rmtree(tg)
|
||||||
shutil.copytree(imfd, tg)
|
shutil.copytree(imfd, tg)
|
||||||
|
for dirpath, dirnames, filenames in os.walk(tdir):
|
||||||
|
for x in filenames:
|
||||||
|
if not x.endswith('.dll'):
|
||||||
|
os.remove(self.j(dirpath, x))
|
||||||
|
|
||||||
print
|
print
|
||||||
print 'Adding third party dependencies'
|
print 'Adding third party dependencies'
|
||||||
@ -254,7 +308,7 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
def embed_resources(self, module, desc=None):
|
def embed_resources(self, module, desc=None):
|
||||||
icon_base = self.j(self.src_root, 'icons')
|
icon_base = self.j(self.src_root, 'icons')
|
||||||
icon_map = {'calibre':'library', 'ebook-viewer':'viewer',
|
icon_map = {'calibre':'library', 'ebook-viewer':'viewer',
|
||||||
'lrfviewer':'viewer'}
|
'lrfviewer':'viewer', 'calibre-portable':'library'}
|
||||||
file_type = 'DLL' if module.endswith('.dll') else 'APP'
|
file_type = 'DLL' if module.endswith('.dll') else 'APP'
|
||||||
template = open(self.rc_template, 'rb').read()
|
template = open(self.rc_template, 'rb').read()
|
||||||
bname = self.b(module)
|
bname = self.b(module)
|
||||||
@ -311,8 +365,62 @@ class Win32Freeze(Command, WixMixIn):
|
|||||||
self.info(p.stderr.read())
|
self.info(p.stderr.read())
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def build_portable(self):
|
||||||
|
base = self.portable_base
|
||||||
|
if os.path.exists(base):
|
||||||
|
shutil.rmtree(base)
|
||||||
|
os.makedirs(base)
|
||||||
|
src = self.j(self.src_root, 'setup', 'installer', 'windows',
|
||||||
|
'portable.c')
|
||||||
|
obj = self.j(self.obj_dir, self.b(src)+'.obj')
|
||||||
|
cflags = '/c /EHsc /MT /W3 /Ox /nologo /D_UNICODE'.split()
|
||||||
|
|
||||||
|
if self.newer(obj, [src]):
|
||||||
|
self.info('Compiling', obj)
|
||||||
|
cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src]
|
||||||
|
self.run_builder(cmd)
|
||||||
|
|
||||||
|
exe = self.j(base, 'calibre-portable.exe')
|
||||||
|
if self.newer(exe, [obj]):
|
||||||
|
self.info('Linking', exe)
|
||||||
|
cmd = [msvc.linker] + ['/INCREMENTAL:NO', '/MACHINE:X86',
|
||||||
|
'/LIBPATH:'+self.obj_dir, '/SUBSYSTEM:WINDOWS',
|
||||||
|
'/RELEASE',
|
||||||
|
'/OUT:'+exe, self.embed_resources(exe),
|
||||||
|
obj, 'User32.lib']
|
||||||
|
self.run_builder(cmd)
|
||||||
|
|
||||||
|
self.info('Creating portable installer')
|
||||||
|
shutil.copytree(self.base, self.j(base, 'Calibre'))
|
||||||
|
os.mkdir(self.j(base, 'Calibre Library'))
|
||||||
|
os.mkdir(self.j(base, 'Calibre Settings'))
|
||||||
|
|
||||||
|
name = '%s-portable-%s.zip'%(__appname__, __version__)
|
||||||
|
with zipfile.ZipFile(self.j('dist', name), 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
self.add_dir_to_zip(zf, base, 'Calibre Portable')
|
||||||
|
|
||||||
|
def add_dir_to_zip(self, zf, path, prefix=''):
|
||||||
|
'''
|
||||||
|
Add a directory recursively to the zip file with an optional prefix.
|
||||||
|
'''
|
||||||
|
if prefix:
|
||||||
|
zi = zipfile.ZipInfo(prefix+'/')
|
||||||
|
zi.external_attr = 16
|
||||||
|
zf.writestr(zi, '')
|
||||||
|
cwd = os.path.abspath(os.getcwd())
|
||||||
|
try:
|
||||||
|
os.chdir(path)
|
||||||
|
fp = (prefix + ('/' if prefix else '')).replace('//', '/')
|
||||||
|
for f in os.listdir('.'):
|
||||||
|
arcname = fp + f
|
||||||
|
if os.path.isdir(f):
|
||||||
|
self.add_dir_to_zip(zf, f, prefix=arcname)
|
||||||
|
else:
|
||||||
|
zf.write(f, arcname)
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
def build_launchers(self):
|
def build_launchers(self):
|
||||||
self.obj_dir = self.j(self.src_root, 'build', 'launcher')
|
|
||||||
if not os.path.exists(self.obj_dir):
|
if not os.path.exists(self.obj_dir):
|
||||||
os.makedirs(self.obj_dir)
|
os.makedirs(self.obj_dir)
|
||||||
base = self.j(self.src_root, 'setup', 'installer', 'windows')
|
base = self.j(self.src_root, 'setup', 'installer', 'windows')
|
||||||
|
146
setup/installer/windows/portable.c
Normal file
146
setup/installer/windows/portable.c
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#ifndef UNICODE
|
||||||
|
#define UNICODE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <tchar.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define BUFSIZE 4096
|
||||||
|
|
||||||
|
void show_error(LPCTSTR msg) {
|
||||||
|
MessageBeep(MB_ICONERROR);
|
||||||
|
MessageBox(NULL, msg, TEXT("Error"), MB_OK|MB_ICONERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_detailed_error(LPCTSTR preamble, LPCTSTR msg, int code) {
|
||||||
|
LPTSTR buf;
|
||||||
|
buf = (LPTSTR)LocalAlloc(LMEM_ZEROINIT, sizeof(TCHAR)*
|
||||||
|
(_tcslen(msg) + _tcslen(preamble) + 80));
|
||||||
|
|
||||||
|
_sntprintf_s(buf,
|
||||||
|
LocalSize(buf) / sizeof(TCHAR), _TRUNCATE,
|
||||||
|
TEXT("%s\r\n %s (Error Code: %d)\r\n"),
|
||||||
|
preamble, msg, code);
|
||||||
|
|
||||||
|
show_error(buf);
|
||||||
|
LocalFree(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_last_error_crt(LPCTSTR preamble) {
|
||||||
|
TCHAR buf[BUFSIZE];
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
_get_errno(&err);
|
||||||
|
_wcserror_s(buf, BUFSIZE, err);
|
||||||
|
show_detailed_error(preamble, buf, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_last_error(LPCTSTR preamble) {
|
||||||
|
TCHAR *msg = NULL;
|
||||||
|
DWORD dw = GetLastError();
|
||||||
|
|
||||||
|
FormatMessage(
|
||||||
|
FORMAT_MESSAGE_ALLOCATE_BUFFER |
|
||||||
|
FORMAT_MESSAGE_FROM_SYSTEM |
|
||||||
|
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
NULL,
|
||||||
|
dw,
|
||||||
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
|
&msg,
|
||||||
|
0, NULL );
|
||||||
|
|
||||||
|
show_detailed_error(preamble, msg, (int)dw);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LPTSTR get_app_dir() {
|
||||||
|
LPTSTR buf, buf2, buf3;
|
||||||
|
DWORD sz;
|
||||||
|
TCHAR drive[4] = TEXT("\0\0\0");
|
||||||
|
errno_t err;
|
||||||
|
|
||||||
|
buf = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
buf2 = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
buf3 = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
|
||||||
|
sz = GetModuleFileName(NULL, buf, BUFSIZE);
|
||||||
|
|
||||||
|
if (sz == 0 || sz > BUFSIZE-1) {
|
||||||
|
show_error(TEXT("Failed to get path to calibre-portable.exe"));
|
||||||
|
ExitProcess(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = _tsplitpath_s(buf, drive, 4, buf2, BUFSIZE, NULL, 0, NULL, 0);
|
||||||
|
|
||||||
|
if (err != 0) {
|
||||||
|
show_last_error_crt(TEXT("Failed to split path to calibre-portable.exe"));
|
||||||
|
ExitProcess(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sntprintf_s(buf3, BUFSIZE-1, _TRUNCATE, TEXT("%s%s"), drive, buf2);
|
||||||
|
free(buf); free(buf2);
|
||||||
|
return buf3;
|
||||||
|
}
|
||||||
|
|
||||||
|
void launch_calibre(LPCTSTR exe, LPCTSTR config_dir, LPCTSTR library_dir) {
|
||||||
|
DWORD dwFlags=0;
|
||||||
|
STARTUPINFO si;
|
||||||
|
PROCESS_INFORMATION pi;
|
||||||
|
BOOL fSuccess;
|
||||||
|
TCHAR cmdline[BUFSIZE];
|
||||||
|
|
||||||
|
if (! SetEnvironmentVariable(TEXT("CALIBRE_CONFIG_DIRECTORY"), config_dir)) {
|
||||||
|
show_last_error(TEXT("Failed to set environment variables"));
|
||||||
|
ExitProcess(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dwFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP;
|
||||||
|
_sntprintf_s(cmdline, BUFSIZE, _TRUNCATE, TEXT(" \"--with-library=%s\""), library_dir);
|
||||||
|
|
||||||
|
ZeroMemory( &si, sizeof(si) );
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
ZeroMemory( &pi, sizeof(pi) );
|
||||||
|
|
||||||
|
fSuccess = CreateProcess(exe, cmdline,
|
||||||
|
NULL, // Process handle not inheritable
|
||||||
|
NULL, // Thread handle not inheritable
|
||||||
|
FALSE, // Set handle inheritance to FALSE
|
||||||
|
dwFlags, // Creation flags http://msdn.microsoft.com/en-us/library/ms684863(v=vs.85).aspx
|
||||||
|
NULL, // Use parent's environment block
|
||||||
|
NULL, // Use parent's starting directory
|
||||||
|
&si, // Pointer to STARTUPINFO structure
|
||||||
|
&pi // Pointer to PROCESS_INFORMATION structure
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fSuccess == 0) {
|
||||||
|
show_last_error(TEXT("Failed to launch the calibre program"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close process and thread handles.
|
||||||
|
CloseHandle( pi.hProcess );
|
||||||
|
CloseHandle( pi.hThread );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
||||||
|
{
|
||||||
|
LPTSTR app_dir, config_dir, exe, library_dir;
|
||||||
|
|
||||||
|
app_dir = get_app_dir();
|
||||||
|
config_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
library_dir = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
exe = (LPTSTR)calloc(BUFSIZE, sizeof(TCHAR));
|
||||||
|
|
||||||
|
_sntprintf_s(config_dir, BUFSIZE, _TRUNCATE, TEXT("%sCalibre Settings"), app_dir);
|
||||||
|
_sntprintf_s(exe, BUFSIZE, _TRUNCATE, TEXT("%sCalibre\\calibre.exe"), app_dir);
|
||||||
|
_sntprintf_s(library_dir, BUFSIZE, _TRUNCATE, TEXT("%sCalibre Library"), app_dir);
|
||||||
|
|
||||||
|
launch_calibre(exe, config_dir, library_dir);
|
||||||
|
|
||||||
|
free(app_dir); free(config_dir); free(exe); free(library_dir);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ def aliasmbcs():
|
|||||||
|
|
||||||
def add_calibre_vars():
|
def add_calibre_vars():
|
||||||
sys.resources_location = os.path.join(sys.app_dir, 'resources')
|
sys.resources_location = os.path.join(sys.app_dir, 'resources')
|
||||||
sys.extensions_location = os.path.join(sys.app_dir, 'plugins')
|
sys.extensions_location = os.path.join(sys.app_dir, 'plugins2')
|
||||||
|
|
||||||
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||||
if dv and os.path.exists(dv):
|
if dv and os.path.exists(dv):
|
||||||
|
@ -173,7 +173,7 @@ int show_last_error(wchar_t *preamble) {
|
|||||||
NULL,
|
NULL,
|
||||||
dw,
|
dw,
|
||||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||||
msg,
|
&msg,
|
||||||
0, NULL );
|
0, NULL );
|
||||||
|
|
||||||
return _show_error(preamble, msg, (int)dw);
|
return _show_error(preamble, msg, (int)dw);
|
||||||
|
@ -11,6 +11,10 @@
|
|||||||
SummaryCodepage='1252' />
|
SummaryCodepage='1252' />
|
||||||
|
|
||||||
<Media Id="1" Cabinet="{app}.cab" CompressionLevel="{compression}" EmbedCab="yes" />
|
<Media Id="1" Cabinet="{app}.cab" CompressionLevel="{compression}" EmbedCab="yes" />
|
||||||
|
<!-- The following line ensures that DLLs are replaced even if their version is the same as before. This
|
||||||
|
is necessary because of the manifest nuking that was part of making calibre isolated. But I think it
|
||||||
|
is more rigorous anyway. -->
|
||||||
|
<Property Id='REINSTALLMODE' Value='emus'/>
|
||||||
|
|
||||||
<Upgrade Id="{upgrade_code}">
|
<Upgrade Id="{upgrade_code}">
|
||||||
<UpgradeVersion Maximum="{version}"
|
<UpgradeVersion Maximum="{version}"
|
||||||
@ -33,7 +37,6 @@
|
|||||||
</Property>
|
</Property>
|
||||||
|
|
||||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||||
<Merge Id="VCRedist" SourceFile="{crt_msm}" DiskId="1" Language="0"/>
|
|
||||||
<Directory Id='ProgramFilesFolder' Name='PFiles'>
|
<Directory Id='ProgramFilesFolder' Name='PFiles'>
|
||||||
<Directory Id='APPLICATIONFOLDER' Name='{app}' />
|
<Directory Id='APPLICATIONFOLDER' Name='{app}' />
|
||||||
</Directory>
|
</Directory>
|
||||||
@ -100,10 +103,6 @@
|
|||||||
<ComponentRef Id="RememberInstallDir"/>
|
<ComponentRef Id="RememberInstallDir"/>
|
||||||
</Feature>
|
</Feature>
|
||||||
|
|
||||||
<Feature Id="VCRedist" Title="Visual C++ 8.0 Runtime" AllowAdvertise="no" Display="hidden" Level="1">
|
|
||||||
<MergeRef Id="VCRedist"/>
|
|
||||||
</Feature>
|
|
||||||
|
|
||||||
<Feature Id="FSMS" Title="Start menu shortcuts" Level="1"
|
<Feature Id="FSMS" Title="Start menu shortcuts" Level="1"
|
||||||
Description="Program shortcuts installed in the Start Menu">
|
Description="Program shortcuts installed in the Start Menu">
|
||||||
<ComponentRef Id="StartMenuShortcuts"/>
|
<ComponentRef Id="StartMenuShortcuts"/>
|
||||||
@ -149,12 +148,13 @@
|
|||||||
Set default folder name and allow only per machine installs.
|
Set default folder name and allow only per machine installs.
|
||||||
For a per-machine installation, the default installation location
|
For a per-machine installation, the default installation location
|
||||||
will be [ProgramFilesFolder][ApplicationFolderName] and the user
|
will be [ProgramFilesFolder][ApplicationFolderName] and the user
|
||||||
will be able to change it in the setup UI. This is because the installer
|
will be able to change it in the setup UI. This is no longer necessary
|
||||||
has to install the VC90 merge module into the system winsxs folder for python
|
(i.e. per user installs should work) but left this way as I
|
||||||
to work, so per user installs are impossible anyway.
|
dont want to deal with the complications
|
||||||
-->
|
-->
|
||||||
<Property Id="ApplicationFolderName" Value="Calibre2" />
|
<Property Id="ApplicationFolderName" Value="Calibre2" />
|
||||||
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
|
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
|
||||||
|
<Property Id="ALLUSERS" Value="1" />
|
||||||
<WixVariable Id="WixUISupportPerUser" Value="0" />
|
<WixVariable Id="WixUISupportPerUser" Value="0" />
|
||||||
|
|
||||||
<!-- Add option to launch calibre after install -->
|
<!-- Add option to launch calibre after install -->
|
||||||
|
@ -35,7 +35,6 @@ class WixMixIn:
|
|||||||
exe_map = self.smap,
|
exe_map = self.smap,
|
||||||
main_icon = self.j(self.src_root, 'icons', 'library.ico'),
|
main_icon = self.j(self.src_root, 'icons', 'library.ico'),
|
||||||
web_icon = self.j(self.src_root, 'icons', 'web.ico'),
|
web_icon = self.j(self.src_root, 'icons', 'web.ico'),
|
||||||
crt_msm = self.j(self.SW, 'Microsoft_VC90_CRT_x86.msm')
|
|
||||||
)
|
)
|
||||||
template = open(self.j(self.d(__file__), 'en-us.xml'),
|
template = open(self.j(self.d(__file__), 'en-us.xml'),
|
||||||
'rb').read()
|
'rb').read()
|
||||||
|
@ -26,6 +26,7 @@ def installers():
|
|||||||
installers = list(map(installer_name, ('dmg', 'msi', 'tar.bz2')))
|
installers = list(map(installer_name, ('dmg', 'msi', 'tar.bz2')))
|
||||||
installers.append(installer_name('tar.bz2', is64bit=True))
|
installers.append(installer_name('tar.bz2', is64bit=True))
|
||||||
installers.insert(0, 'dist/%s-%s.tar.gz'%(__appname__, __version__))
|
installers.insert(0, 'dist/%s-%s.tar.gz'%(__appname__, __version__))
|
||||||
|
installers.append('dist/%s-portable-%s.zip'%(__appname__, __version__))
|
||||||
return installers
|
return installers
|
||||||
|
|
||||||
def installer_description(fname):
|
def installer_description(fname):
|
||||||
@ -38,6 +39,8 @@ def installer_description(fname):
|
|||||||
return 'Windows installer'
|
return 'Windows installer'
|
||||||
if fname.endswith('.dmg'):
|
if fname.endswith('.dmg'):
|
||||||
return 'OS X dmg'
|
return 'OS X dmg'
|
||||||
|
if fname.endswith('.zip'):
|
||||||
|
return 'Calibre Portable'
|
||||||
return 'Unknown file'
|
return 'Unknown file'
|
||||||
|
|
||||||
class ReUpload(Command): # {{{
|
class ReUpload(Command): # {{{
|
||||||
@ -90,9 +93,11 @@ class UploadToGoogleCode(Command): # {{{
|
|||||||
|
|
||||||
def upload_one(self, fname):
|
def upload_one(self, fname):
|
||||||
self.info('Uploading', fname)
|
self.info('Uploading', fname)
|
||||||
typ = 'Type-Source' if fname.endswith('.gz') else 'Type-Installer'
|
typ = 'Type-' + ('Source' if fname.endswith('.gz') else 'Archive' if
|
||||||
|
fname.endswith('.zip') else 'Installer')
|
||||||
ext = os.path.splitext(fname)[1][1:]
|
ext = os.path.splitext(fname)[1][1:]
|
||||||
op = 'OpSys-'+{'msi':'Windows','dmg':'OSX','bz2':'Linux','gz':'All'}[ext]
|
op = 'OpSys-'+{'msi':'Windows','zip':'Windows',
|
||||||
|
'dmg':'OSX','bz2':'Linux','gz':'All'}[ext]
|
||||||
desc = installer_description(fname)
|
desc = installer_description(fname)
|
||||||
start = time.time()
|
start = time.time()
|
||||||
path = self.upload(os.path.abspath(fname), desc,
|
path = self.upload(os.path.abspath(fname), desc,
|
||||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = u'calibre'
|
__appname__ = u'calibre'
|
||||||
numeric_version = (0, 8, 4)
|
numeric_version = (0, 8, 5)
|
||||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
|
@ -586,10 +586,10 @@ from calibre.devices.apple.driver import ITUNES
|
|||||||
from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX, SPECTRA
|
from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX, SPECTRA
|
||||||
from calibre.devices.blackberry.driver import BLACKBERRY
|
from calibre.devices.blackberry.driver import BLACKBERRY
|
||||||
from calibre.devices.cybook.driver import CYBOOK, ORIZON
|
from calibre.devices.cybook.driver import CYBOOK, ORIZON
|
||||||
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
|
from calibre.devices.eb600.driver import (EB600, COOL_ER, SHINEBOOK,
|
||||||
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
|
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK,
|
||||||
BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602, \
|
BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602,
|
||||||
POCKETBOOK701
|
POCKETBOOK701, POCKETBOOK360P)
|
||||||
from calibre.devices.iliad.driver import ILIAD
|
from calibre.devices.iliad.driver import ILIAD
|
||||||
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
||||||
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
|
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
|
||||||
@ -603,10 +603,11 @@ from calibre.devices.eslick.driver import ESLICK, EBK52
|
|||||||
from calibre.devices.nuut2.driver import NUUT2
|
from calibre.devices.nuut2.driver import NUUT2
|
||||||
from calibre.devices.iriver.driver import IRIVER_STORY
|
from calibre.devices.iriver.driver import IRIVER_STORY
|
||||||
from calibre.devices.binatone.driver import README
|
from calibre.devices.binatone.driver import README
|
||||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
from calibre.devices.hanvon.driver import (N516, EB511, ALEX, AZBOOKA, THEBOOK,
|
||||||
|
LIBREAIR)
|
||||||
from calibre.devices.edge.driver import EDGE
|
from calibre.devices.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
|
from calibre.devices.teclast.driver import (TECLAST_K3, NEWSMY, IPAPYRUS,
|
||||||
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER
|
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER)
|
||||||
from calibre.devices.sne.driver import SNE
|
from calibre.devices.sne.driver import SNE
|
||||||
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
|
from calibre.devices.misc import (PALMPRE, AVANT, SWEEX, PDNOVEL,
|
||||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
|
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR,
|
||||||
@ -689,7 +690,7 @@ plugins += [
|
|||||||
JETBOOK_MINI,
|
JETBOOK_MINI,
|
||||||
MIBUK,
|
MIBUK,
|
||||||
SHINEBOOK,
|
SHINEBOOK,
|
||||||
POCKETBOOK360, POCKETBOOK301, POCKETBOOK602, POCKETBOOK701,
|
POCKETBOOK360, POCKETBOOK301, POCKETBOOK602, POCKETBOOK701, POCKETBOOK360P,
|
||||||
KINDLE,
|
KINDLE,
|
||||||
KINDLE2,
|
KINDLE2,
|
||||||
KINDLE_DX,
|
KINDLE_DX,
|
||||||
@ -716,7 +717,7 @@ plugins += [
|
|||||||
EB600,
|
EB600,
|
||||||
README,
|
README,
|
||||||
N516,
|
N516,
|
||||||
THEBOOK,
|
THEBOOK, LIBREAIR,
|
||||||
EB511,
|
EB511,
|
||||||
ELONEX,
|
ELONEX,
|
||||||
TECLAST_K3,
|
TECLAST_K3,
|
||||||
@ -1418,6 +1419,15 @@ class StoreWoblinkStore(StoreBase):
|
|||||||
headquarters = 'PL'
|
headquarters = 'PL'
|
||||||
formats = ['EPUB']
|
formats = ['EPUB']
|
||||||
|
|
||||||
|
class StoreZixoStore(StoreBase):
|
||||||
|
name = 'Zixo'
|
||||||
|
author = u'Tomasz Długosz'
|
||||||
|
description = u'Księgarnia z ebookami oraz książkami audio. Aby otwierać książki w formacie Zixo należy zainstalować program dostępny na stronie księgarni. Umożliwia on m.in. dodawanie zakładek i dostosowywanie rozmiaru czcionki.'
|
||||||
|
actual_plugin = 'calibre.gui2.store.zixo_plugin:ZixoStore'
|
||||||
|
|
||||||
|
headquarters = 'PL'
|
||||||
|
formats = ['PDF, ZIXO']
|
||||||
|
|
||||||
plugins += [
|
plugins += [
|
||||||
StoreArchiveOrgStore,
|
StoreArchiveOrgStore,
|
||||||
StoreAmazonKindleStore,
|
StoreAmazonKindleStore,
|
||||||
@ -1452,7 +1462,8 @@ plugins += [
|
|||||||
StoreWeightlessBooksStore,
|
StoreWeightlessBooksStore,
|
||||||
StoreWHSmithUKStore,
|
StoreWHSmithUKStore,
|
||||||
StoreWizardsTowerBooksStore,
|
StoreWizardsTowerBooksStore,
|
||||||
StoreWoblinkStore
|
StoreWoblinkStore,
|
||||||
|
StoreZixoStore
|
||||||
]
|
]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -355,11 +355,17 @@ def remove_plugin(plugin_or_name):
|
|||||||
name = getattr(plugin_or_name, 'name', plugin_or_name)
|
name = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||||
plugins = config['plugins']
|
plugins = config['plugins']
|
||||||
removed = False
|
removed = False
|
||||||
if name in plugins.keys():
|
if name in plugins:
|
||||||
removed = True
|
removed = True
|
||||||
zfp = plugins[name]
|
try:
|
||||||
if os.path.exists(zfp):
|
zfp = os.path.join(plugin_dir, name+'.zip')
|
||||||
os.remove(zfp)
|
if os.path.exists(zfp):
|
||||||
|
os.remove(zfp)
|
||||||
|
zfp = plugins[name]
|
||||||
|
if os.path.exists(zfp):
|
||||||
|
os.remove(zfp)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
plugins.pop(name)
|
plugins.pop(name)
|
||||||
config['plugins'] = plugins
|
config['plugins'] = plugins
|
||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
@ -495,8 +501,15 @@ def initialize_plugins():
|
|||||||
builtin_names]
|
builtin_names]
|
||||||
for p in conflicts:
|
for p in conflicts:
|
||||||
remove_plugin(p)
|
remove_plugin(p)
|
||||||
for zfp in list(config['plugins'].itervalues()) + builtin_plugins:
|
external_plugins = config['plugins']
|
||||||
|
for zfp in list(external_plugins) + builtin_plugins:
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(zfp, type):
|
||||||
|
# We have a plugin name
|
||||||
|
pname = zfp
|
||||||
|
zfp = os.path.join(plugin_dir, zfp+'.zip')
|
||||||
|
if not os.path.exists(zfp):
|
||||||
|
zfp = external_plugins[pname]
|
||||||
try:
|
try:
|
||||||
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
||||||
except PluginNotFound:
|
except PluginNotFound:
|
||||||
|
@ -52,6 +52,7 @@ class ANDROID(USBMS):
|
|||||||
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
|
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
|
||||||
0x681c : [0x0222, 0x0224, 0x0400],
|
0x681c : [0x0222, 0x0224, 0x0400],
|
||||||
0x6640 : [0x0100],
|
0x6640 : [0x0100],
|
||||||
|
0x685b : [0x0400],
|
||||||
0x685e : [0x0400],
|
0x685e : [0x0400],
|
||||||
0x6860 : [0x0400],
|
0x6860 : [0x0400],
|
||||||
0x6877 : [0x0400],
|
0x6877 : [0x0400],
|
||||||
@ -93,6 +94,9 @@ class ANDROID(USBMS):
|
|||||||
# CREEL?? Also Nextbook
|
# CREEL?? Also Nextbook
|
||||||
0x5e3 : { 0x726 : [0x222] },
|
0x5e3 : { 0x726 : [0x222] },
|
||||||
|
|
||||||
|
# ZTE
|
||||||
|
0x19d2 : { 0x1353 : [0x226] },
|
||||||
|
|
||||||
}
|
}
|
||||||
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
||||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
|
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
|
||||||
@ -103,7 +107,7 @@ class ANDROID(USBMS):
|
|||||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
||||||
'GENERIC-']
|
'GENERIC-', 'ZTE']
|
||||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||||
|
@ -246,6 +246,16 @@ class POCKETBOOK602(USBMS):
|
|||||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902',
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902',
|
||||||
'PB903', 'PB']
|
'PB903', 'PB']
|
||||||
|
|
||||||
|
class POCKETBOOK360P(POCKETBOOK602):
|
||||||
|
|
||||||
|
name = 'PocketBook 360+ Device Interface'
|
||||||
|
description = _('Communicate with the PocketBook 360+ reader.')
|
||||||
|
BCD = [0x0323]
|
||||||
|
EBOOK_DIR_MAIN = ''
|
||||||
|
|
||||||
|
VENDOR_NAME = '__POCKET'
|
||||||
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'BOOK_USB_STORAGE'
|
||||||
|
|
||||||
class POCKETBOOK701(USBMS):
|
class POCKETBOOK701(USBMS):
|
||||||
|
|
||||||
name = 'PocketBook 701 Device Interface'
|
name = 'PocketBook 701 Device Interface'
|
||||||
|
@ -52,6 +52,18 @@ class THEBOOK(N516):
|
|||||||
EBOOK_DIR_MAIN = 'My books'
|
EBOOK_DIR_MAIN = 'My books'
|
||||||
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
|
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
|
||||||
|
|
||||||
|
class LIBREAIR(N516):
|
||||||
|
name = 'Libre Air Driver'
|
||||||
|
gui_name = 'Libre Air'
|
||||||
|
description = _('Communicate with the Libre Air reader.')
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'rtf', 'txt', 'pdf']
|
||||||
|
|
||||||
|
BCD = [0x399]
|
||||||
|
VENDOR_NAME = 'ALURATEK'
|
||||||
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGET'
|
||||||
|
EBOOK_DIR_MAIN = 'Books'
|
||||||
|
|
||||||
class ALEX(N516):
|
class ALEX(N516):
|
||||||
|
|
||||||
name = 'Alex driver'
|
name = 'Alex driver'
|
||||||
|
@ -21,7 +21,7 @@ class KOBO(USBMS):
|
|||||||
name = 'Kobo Reader Device Interface'
|
name = 'Kobo Reader Device Interface'
|
||||||
gui_name = 'Kobo Reader'
|
gui_name = 'Kobo Reader'
|
||||||
description = _('Communicate with the Kobo Reader')
|
description = _('Communicate with the Kobo Reader')
|
||||||
author = 'Timothy Legge and Kovid Goyal'
|
author = 'Timothy Legge'
|
||||||
version = (1, 0, 9)
|
version = (1, 0, 9)
|
||||||
|
|
||||||
dbversion = 0
|
dbversion = 0
|
||||||
@ -37,8 +37,8 @@ class KOBO(USBMS):
|
|||||||
CAN_SET_METADATA = ['collections']
|
CAN_SET_METADATA = ['collections']
|
||||||
|
|
||||||
VENDOR_ID = [0x2237]
|
VENDOR_ID = [0x2237]
|
||||||
PRODUCT_ID = [0x4161]
|
PRODUCT_ID = [0x4161, 0x4163]
|
||||||
BCD = [0x0110, 0x0323]
|
BCD = [0x0110, 0x0323, 0x0326]
|
||||||
|
|
||||||
VENDOR_NAME = ['KOBO_INC', 'KOBO']
|
VENDOR_NAME = ['KOBO_INC', 'KOBO']
|
||||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
|
||||||
|
@ -77,7 +77,6 @@ class NOOK(USBMS):
|
|||||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||||
coverfile.write(coverdata)
|
coverfile.write(coverdata)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_path_components(self, components):
|
def sanitize_path_components(self, components):
|
||||||
return [x.replace('#', '_') for x in components]
|
return [x.replace('#', '_') for x in components]
|
||||||
|
|
||||||
@ -110,6 +109,11 @@ class NOOK_COLOR(NOOK):
|
|||||||
def upload_cover(self, path, filename, metadata, filepath):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_carda_ebook_dir(self, for_upload=False):
|
||||||
|
if for_upload:
|
||||||
|
return 'My Files/Books'
|
||||||
|
return ''
|
||||||
|
|
||||||
class NOOK_TSR(NOOK):
|
class NOOK_TSR(NOOK):
|
||||||
gui_name = _('Nook Simple')
|
gui_name = _('Nook Simple')
|
||||||
description = _('Communicate with the Nook TSR eBook reader.')
|
description = _('Communicate with the Nook TSR eBook reader.')
|
||||||
@ -117,9 +121,15 @@ class NOOK_TSR(NOOK):
|
|||||||
PRODUCT_ID = [0x003]
|
PRODUCT_ID = [0x003]
|
||||||
BCD = [0x216]
|
BCD = [0x216]
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'My Files/Books'
|
EBOOK_DIR_MAIN = 'My Files/Books'
|
||||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'EBOOK_DISK'
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata, filepath):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_carda_ebook_dir(self, for_upload=False):
|
||||||
|
if for_upload:
|
||||||
|
return 'My Files/Books'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@ -837,6 +837,9 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
def get_main_ebook_dir(self, for_upload=False):
|
def get_main_ebook_dir(self, for_upload=False):
|
||||||
return self.EBOOK_DIR_MAIN
|
return self.EBOOK_DIR_MAIN
|
||||||
|
|
||||||
|
def get_carda_ebook_dir(self, for_upload=False):
|
||||||
|
return self.EBOOK_DIR_CARD_A
|
||||||
|
|
||||||
def _sanity_check(self, on_card, files):
|
def _sanity_check(self, on_card, files):
|
||||||
if on_card == 'carda' and not self._card_a_prefix:
|
if on_card == 'carda' and not self._card_a_prefix:
|
||||||
raise ValueError(_('The reader has no storage card in this slot.'))
|
raise ValueError(_('The reader has no storage card in this slot.'))
|
||||||
@ -847,7 +850,7 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
|
|
||||||
if on_card == 'carda':
|
if on_card == 'carda':
|
||||||
path = os.path.join(self._card_a_prefix,
|
path = os.path.join(self._card_a_prefix,
|
||||||
*(self.EBOOK_DIR_CARD_A.split('/')))
|
*(self.get_carda_ebook_dir(for_upload=True).split('/')))
|
||||||
elif on_card == 'cardb':
|
elif on_card == 'cardb':
|
||||||
path = os.path.join(self._card_b_prefix,
|
path = os.path.join(self._card_b_prefix,
|
||||||
*(self.EBOOK_DIR_CARD_B.split('/')))
|
*(self.EBOOK_DIR_CARD_B.split('/')))
|
||||||
|
@ -132,7 +132,7 @@ class USBMS(CLI, Device):
|
|||||||
self._card_b_prefix if oncard == 'cardb' \
|
self._card_b_prefix if oncard == 'cardb' \
|
||||||
else self._main_prefix
|
else self._main_prefix
|
||||||
|
|
||||||
ebook_dirs = self.EBOOK_DIR_CARD_A if oncard == 'carda' else \
|
ebook_dirs = self.get_carda_ebook_dir() if oncard == 'carda' else \
|
||||||
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
|
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
|
||||||
self.get_main_ebook_dir()
|
self.get_main_ebook_dir()
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import os, shutil, traceback, textwrap, time, codecs
|
|||||||
from Queue import Empty
|
from Queue import Empty
|
||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||||
from calibre import extract, CurrentDir, prints
|
from calibre import extract, CurrentDir, prints, walk
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
@ -27,6 +27,11 @@ def extract_comic(path_to_comic_file):
|
|||||||
# names
|
# names
|
||||||
tdir = tdir.decode(filesystem_encoding)
|
tdir = tdir.decode(filesystem_encoding)
|
||||||
extract(path_to_comic_file, tdir)
|
extract(path_to_comic_file, tdir)
|
||||||
|
for x in walk(tdir):
|
||||||
|
bn = os.path.basename(x)
|
||||||
|
nbn = bn.replace('#', '_')
|
||||||
|
if nbn != bn:
|
||||||
|
os.rename(x, os.path.join(os.path.dirname(x), nbn))
|
||||||
return tdir
|
return tdir
|
||||||
|
|
||||||
def find_pages(dir, sort_on_mtime=False, verbose=False):
|
def find_pages(dir, sort_on_mtime=False, verbose=False):
|
||||||
@ -362,6 +367,7 @@ class ComicInput(InputFormatPlugin):
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
fname, title = line.partition(':')[0], line.partition(':')[-1]
|
fname, title = line.partition(':')[0], line.partition(':')[-1]
|
||||||
|
fname = fname.replace('#', '_')
|
||||||
fname = os.path.join(tdir, *fname.split('/'))
|
fname = os.path.join(tdir, *fname.split('/'))
|
||||||
if not title:
|
if not title:
|
||||||
title = os.path.basename(fname).rpartition('.')[0]
|
title = os.path.basename(fname).rpartition('.')[0]
|
||||||
|
@ -394,6 +394,13 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
for tag in XPath('//h:img[@src]')(root):
|
for tag in XPath('//h:img[@src]')(root):
|
||||||
tag.set('src', tag.get('src', '').replace('&', ''))
|
tag.set('src', tag.get('src', '').replace('&', ''))
|
||||||
|
|
||||||
|
# ADE whimpers in fright when it encounters a <td> outside a
|
||||||
|
# <table>
|
||||||
|
in_table = XPath('ancestor::h:table')
|
||||||
|
for tag in XPath('//h:td|//h:tr|//h:th')(root):
|
||||||
|
if not in_table(tag):
|
||||||
|
tag.tag = XHTML('div')
|
||||||
|
|
||||||
special_chars = re.compile(u'[\u200b\u00ad]')
|
special_chars = re.compile(u'[\u200b\u00ad]')
|
||||||
for elem in root.iterdescendants():
|
for elem in root.iterdescendants():
|
||||||
if getattr(elem, 'text', False):
|
if getattr(elem, 'text', False):
|
||||||
@ -413,7 +420,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
rule.style.removeProperty('margin-left')
|
rule.style.removeProperty('margin-left')
|
||||||
# padding-left breaks rendering in webkit and gecko
|
# padding-left breaks rendering in webkit and gecko
|
||||||
rule.style.removeProperty('padding-left')
|
rule.style.removeProperty('padding-left')
|
||||||
# Change whitespace:pre to pre-line to accommodate readers that
|
# Change whitespace:pre to pre-wrap to accommodate readers that
|
||||||
# cannot scroll horizontally
|
# cannot scroll horizontally
|
||||||
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
|
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
|
||||||
style = rule.style
|
style = rule.style
|
||||||
|
@ -442,9 +442,12 @@ class MobiMLizer(object):
|
|||||||
if tag in TABLE_TAGS and self.ignore_tables:
|
if tag in TABLE_TAGS and self.ignore_tables:
|
||||||
tag = 'span' if tag == 'td' else 'div'
|
tag = 'span' if tag == 'td' else 'div'
|
||||||
|
|
||||||
# GR: Added 'width', 'border' and 'scope'
|
if tag == 'table':
|
||||||
|
css = style.cssdict()
|
||||||
|
if 'border' in css or 'border-width' in css:
|
||||||
|
elem.set('border', '1')
|
||||||
if tag in TABLE_TAGS:
|
if tag in TABLE_TAGS:
|
||||||
for attr in ('rowspan', 'colspan','width','border','scope'):
|
for attr in ('rowspan', 'colspan', 'width', 'border', 'scope'):
|
||||||
if attr in elem.attrib:
|
if attr in elem.attrib:
|
||||||
istate.attrib[attr] = elem.attrib[attr]
|
istate.attrib[attr] = elem.attrib[attr]
|
||||||
if tag == 'q':
|
if tag == 'q':
|
||||||
|
@ -348,7 +348,6 @@ class MobiReader(object):
|
|||||||
self.processed_html = self.remove_random_bytes(self.processed_html)
|
self.processed_html = self.remove_random_bytes(self.processed_html)
|
||||||
root = soupparser.fromstring(self.processed_html)
|
root = soupparser.fromstring(self.processed_html)
|
||||||
|
|
||||||
|
|
||||||
if root.tag != 'html':
|
if root.tag != 'html':
|
||||||
self.log.warn('File does not have opening <html> tag')
|
self.log.warn('File does not have opening <html> tag')
|
||||||
nroot = html.fromstring('<html><head></head><body></body></html>')
|
nroot = html.fromstring('<html><head></head><body></body></html>')
|
||||||
|
@ -271,6 +271,9 @@ class Dispatcher(QObject):
|
|||||||
Convenience class to use Qt signals with arbitrary python callables.
|
Convenience class to use Qt signals with arbitrary python callables.
|
||||||
By default, ensures that a function call always happens in the
|
By default, ensures that a function call always happens in the
|
||||||
thread this Dispatcher was created in.
|
thread this Dispatcher was created in.
|
||||||
|
|
||||||
|
Note that if you create the Dispatcher in a thread without an event loop of
|
||||||
|
its own, the function call will happen in the GUI thread (I think).
|
||||||
'''
|
'''
|
||||||
dispatch_signal = pyqtSignal(object, object)
|
dispatch_signal = pyqtSignal(object, object)
|
||||||
|
|
||||||
@ -292,11 +295,20 @@ class FunctionDispatcher(QObject):
|
|||||||
'''
|
'''
|
||||||
Convenience class to use Qt signals with arbitrary python functions.
|
Convenience class to use Qt signals with arbitrary python functions.
|
||||||
By default, ensures that a function call always happens in the
|
By default, ensures that a function call always happens in the
|
||||||
thread this Dispatcher was created in.
|
thread this FunctionDispatcher was created in.
|
||||||
|
|
||||||
|
Note that you must create FunctionDispatcher objects in the GUI thread.
|
||||||
'''
|
'''
|
||||||
dispatch_signal = pyqtSignal(object, object, object)
|
dispatch_signal = pyqtSignal(object, object, object)
|
||||||
|
|
||||||
def __init__(self, func, queued=True, parent=None):
|
def __init__(self, func, queued=True, parent=None):
|
||||||
|
global gui_thread
|
||||||
|
if gui_thread is None:
|
||||||
|
gui_thread = QThread.currentThread()
|
||||||
|
if not is_gui_thread():
|
||||||
|
raise ValueError(
|
||||||
|
'You can only create a FunctionDispatcher in the GUI thread')
|
||||||
|
|
||||||
QObject.__init__(self, parent)
|
QObject.__init__(self, parent)
|
||||||
self.func = func
|
self.func = func
|
||||||
typ = Qt.QueuedConnection
|
typ = Qt.QueuedConnection
|
||||||
@ -307,6 +319,8 @@ class FunctionDispatcher(QObject):
|
|||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
|
if is_gui_thread():
|
||||||
|
return self.func(*args, **kwargs)
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.dispatch_signal.emit(self.q, args, kwargs)
|
self.dispatch_signal.emit(self.q, args, kwargs)
|
||||||
res = self.q.get()
|
res = self.q.get()
|
||||||
|
@ -11,10 +11,11 @@ from functools import partial
|
|||||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
||||||
|
|
||||||
from calibre import isbytestring
|
from calibre import isbytestring
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding, iswindows
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
|
from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog,
|
||||||
question_dialog, info_dialog
|
question_dialog, info_dialog)
|
||||||
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
class LibraryUsageStats(object): # {{{
|
class LibraryUsageStats(object): # {{{
|
||||||
@ -229,6 +230,12 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
return error_dialog(self.gui, _('Already exists'),
|
return error_dialog(self.gui, _('Already exists'),
|
||||||
_('The folder %s already exists. Delete it first.') %
|
_('The folder %s already exists. Delete it first.') %
|
||||||
newloc, show=True)
|
newloc, show=True)
|
||||||
|
if (iswindows and len(newloc) >
|
||||||
|
LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT):
|
||||||
|
return error_dialog(self.gui, _('Too long'),
|
||||||
|
_('Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT,
|
||||||
|
show=True)
|
||||||
try:
|
try:
|
||||||
os.rename(loc, newloc)
|
os.rename(loc, newloc)
|
||||||
except:
|
except:
|
||||||
|
@ -439,7 +439,8 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
view.reset()
|
view.reset()
|
||||||
|
|
||||||
# Apply bulk metadata changes {{{
|
# Apply bulk metadata changes {{{
|
||||||
def apply_metadata_changes(self, id_map, title=None, msg='', callback=None):
|
def apply_metadata_changes(self, id_map, title=None, msg='', callback=None,
|
||||||
|
merge_tags=True):
|
||||||
'''
|
'''
|
||||||
Apply the metadata changes in id_map to the database synchronously
|
Apply the metadata changes in id_map to the database synchronously
|
||||||
id_map must be a mapping of ids to Metadata objects. Set any fields you
|
id_map must be a mapping of ids to Metadata objects. Set any fields you
|
||||||
@ -466,9 +467,9 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
cancelable=False)
|
cancelable=False)
|
||||||
self.apply_pd.setModal(True)
|
self.apply_pd.setModal(True)
|
||||||
self.apply_pd.show()
|
self.apply_pd.show()
|
||||||
|
self._am_merge_tags = True
|
||||||
self.do_one_apply()
|
self.do_one_apply()
|
||||||
|
|
||||||
|
|
||||||
def do_one_apply(self):
|
def do_one_apply(self):
|
||||||
if self.apply_current_idx >= len(self.apply_id_map):
|
if self.apply_current_idx >= len(self.apply_id_map):
|
||||||
return self.finalize_apply()
|
return self.finalize_apply()
|
||||||
@ -484,6 +485,12 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
mi.identifiers = idents
|
mi.identifiers = idents
|
||||||
if mi.is_null('series'):
|
if mi.is_null('series'):
|
||||||
mi.series_index = None
|
mi.series_index = None
|
||||||
|
if self._am_merge_tags:
|
||||||
|
old_tags = db.tags(i, index_is_id=True)
|
||||||
|
if old_tags:
|
||||||
|
tags = [x.strip() for x in old_tags.split(',')] + (
|
||||||
|
mi.tags if mi.tags else [])
|
||||||
|
mi.tags = list(set(tags))
|
||||||
db.set_metadata(i, mi, commit=False, set_title=set_title,
|
db.set_metadata(i, mi, commit=False, set_title=set_title,
|
||||||
set_authors=set_authors, notify=False)
|
set_authors=set_authors, notify=False)
|
||||||
self.applied_ids.append(i)
|
self.applied_ids.append(i)
|
||||||
|
@ -33,7 +33,6 @@ class SaveMenu(QMenu): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
class SaveToDiskAction(InterfaceAction):
|
class SaveToDiskAction(InterfaceAction):
|
||||||
|
|
||||||
name = "Save To Disk"
|
name = "Save To Disk"
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
<string> KB</string>
|
<string> KB</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimum">
|
<property name="minimum">
|
||||||
<number>100</number>
|
<number>25</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>1000000</number>
|
<number>1000000</number>
|
||||||
|
@ -6,18 +6,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, traceback, Queue, time, cStringIO, re, sys
|
import os, traceback, Queue, time, cStringIO, re, sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, \
|
from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL,
|
||||||
Qt, pyqtSignal, QDialog, QObject
|
Qt, pyqtSignal, QDialog, QObject)
|
||||||
|
|
||||||
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
from calibre.customize.ui import (available_input_formats, available_output_formats,
|
||||||
device_plugins
|
device_plugins)
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
from calibre.devices.errors import UserFeedback, OpenFeedback
|
from calibre.devices.errors import UserFeedback, OpenFeedback
|
||||||
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
|
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
|
||||||
from calibre.utils.ipc.job import BaseJob
|
from calibre.utils.ipc.job import BaseJob
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
from calibre.gui2 import (config, error_dialog, Dispatcher, dynamic,
|
||||||
warning_dialog, info_dialog, choose_dir
|
warning_dialog, info_dialog, choose_dir, FunctionDispatcher)
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre import preferred_encoding, prints, force_unicode, as_unicode
|
from calibre import preferred_encoding, prints, force_unicode, as_unicode
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
@ -35,8 +35,13 @@ class DeviceJob(BaseJob): # {{{
|
|||||||
|
|
||||||
def __init__(self, func, done, job_manager, args=[], kwargs={},
|
def __init__(self, func, done, job_manager, args=[], kwargs={},
|
||||||
description=''):
|
description=''):
|
||||||
BaseJob.__init__(self, description, done=done)
|
BaseJob.__init__(self, description)
|
||||||
self.func = func
|
self.func = func
|
||||||
|
self.callback_on_done = done
|
||||||
|
if not isinstance(self.callback_on_done, (Dispatcher,
|
||||||
|
FunctionDispatcher)):
|
||||||
|
self.callback_on_done = FunctionDispatcher(self.callback_on_done)
|
||||||
|
|
||||||
self.args, self.kwargs = args, kwargs
|
self.args, self.kwargs = args, kwargs
|
||||||
self.exception = None
|
self.exception = None
|
||||||
self.job_manager = job_manager
|
self.job_manager = job_manager
|
||||||
@ -50,6 +55,10 @@ class DeviceJob(BaseJob): # {{{
|
|||||||
def job_done(self):
|
def job_done(self):
|
||||||
self.duration = time.time() - self.start_time
|
self.duration = time.time() - self.start_time
|
||||||
self.percent = 1
|
self.percent = 1
|
||||||
|
try:
|
||||||
|
self.callback_on_done(self)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
self.job_manager.changed_queue.put(self)
|
self.job_manager.changed_queue.put(self)
|
||||||
|
|
||||||
def report_progress(self, percent, msg=''):
|
def report_progress(self, percent, msg=''):
|
||||||
@ -254,7 +263,8 @@ class DeviceManager(Thread): # {{{
|
|||||||
job = self.next()
|
job = self.next()
|
||||||
if job is not None:
|
if job is not None:
|
||||||
self.current_job = job
|
self.current_job = job
|
||||||
self.device.set_progress_reporter(job.report_progress)
|
if self.device is not None:
|
||||||
|
self.device.set_progress_reporter(job.report_progress)
|
||||||
self.current_job.run()
|
self.current_job.run()
|
||||||
self.current_job = None
|
self.current_job = None
|
||||||
else:
|
else:
|
||||||
@ -587,7 +597,7 @@ class DeviceMenu(QMenu): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class DeviceSignals(QObject):
|
class DeviceSignals(QObject): # {{{
|
||||||
#: This signal is emitted once, after metadata is downloaded from the
|
#: This signal is emitted once, after metadata is downloaded from the
|
||||||
#: connected device.
|
#: connected device.
|
||||||
#: The sequence: gui.device_manager.is_device_connected will become True,
|
#: The sequence: gui.device_manager.is_device_connected will become True,
|
||||||
@ -604,6 +614,7 @@ class DeviceSignals(QObject):
|
|||||||
device_connection_changed = pyqtSignal(object)
|
device_connection_changed = pyqtSignal(object)
|
||||||
|
|
||||||
device_signals = DeviceSignals()
|
device_signals = DeviceSignals()
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceMixin(object): # {{{
|
class DeviceMixin(object): # {{{
|
||||||
|
|
||||||
@ -611,7 +622,7 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_error_dialog = error_dialog(self, _('Error'),
|
self.device_error_dialog = error_dialog(self, _('Error'),
|
||||||
_('Error communicating with device'), ' ')
|
_('Error communicating with device'), ' ')
|
||||||
self.device_error_dialog.setModal(Qt.NonModal)
|
self.device_error_dialog.setModal(Qt.NonModal)
|
||||||
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
self.device_manager = DeviceManager(FunctionDispatcher(self.device_detected),
|
||||||
self.job_manager, Dispatcher(self.status_bar.show_message),
|
self.job_manager, Dispatcher(self.status_bar.show_message),
|
||||||
Dispatcher(self.show_open_feedback))
|
Dispatcher(self.show_open_feedback))
|
||||||
self.device_manager.start()
|
self.device_manager.start()
|
||||||
@ -736,7 +747,7 @@ class DeviceMixin(object): # {{{
|
|||||||
self.set_device_menu_items_state(connected)
|
self.set_device_menu_items_state(connected)
|
||||||
if connected:
|
if connected:
|
||||||
self.device_manager.get_device_information(\
|
self.device_manager.get_device_information(\
|
||||||
Dispatcher(self.info_read))
|
FunctionDispatcher(self.info_read))
|
||||||
self.set_default_thumbnail(\
|
self.set_default_thumbnail(\
|
||||||
self.device_manager.device.THUMBNAIL_HEIGHT)
|
self.device_manager.device.THUMBNAIL_HEIGHT)
|
||||||
self.status_bar.show_message(_('Device: ')+\
|
self.status_bar.show_message(_('Device: ')+\
|
||||||
@ -767,7 +778,7 @@ class DeviceMixin(object): # {{{
|
|||||||
self.device_manager.device.icon)
|
self.device_manager.device.icon)
|
||||||
self.bars_manager.update_bars()
|
self.bars_manager.update_bars()
|
||||||
self.status_bar.device_connected(info[0])
|
self.status_bar.device_connected(info[0])
|
||||||
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
self.device_manager.books(FunctionDispatcher(self.metadata_downloaded))
|
||||||
|
|
||||||
def metadata_downloaded(self, job):
|
def metadata_downloaded(self, job):
|
||||||
'''
|
'''
|
||||||
@ -810,7 +821,7 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
def remove_paths(self, paths):
|
def remove_paths(self, paths):
|
||||||
return self.device_manager.delete_books(
|
return self.device_manager.delete_books(
|
||||||
Dispatcher(self.books_deleted), paths)
|
FunctionDispatcher(self.books_deleted), paths)
|
||||||
|
|
||||||
def books_deleted(self, job):
|
def books_deleted(self, job):
|
||||||
'''
|
'''
|
||||||
@ -1187,7 +1198,7 @@ class DeviceMixin(object): # {{{
|
|||||||
Upload metadata to device.
|
Upload metadata to device.
|
||||||
'''
|
'''
|
||||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
|
self.device_manager.sync_booklists(FunctionDispatcher(self.metadata_synced),
|
||||||
self.booklists(), plugboards)
|
self.booklists(), plugboards)
|
||||||
|
|
||||||
def metadata_synced(self, job):
|
def metadata_synced(self, job):
|
||||||
@ -1222,7 +1233,7 @@ class DeviceMixin(object): # {{{
|
|||||||
titles = [i.title for i in metadata]
|
titles = [i.title for i in metadata]
|
||||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
job = self.device_manager.upload_books(
|
job = self.device_manager.upload_books(
|
||||||
Dispatcher(self.books_uploaded),
|
FunctionDispatcher(self.books_uploaded),
|
||||||
files, names, on_card=on_card,
|
files, names, on_card=on_card,
|
||||||
metadata=metadata, titles=titles, plugboards=plugboards
|
metadata=metadata, titles=titles, plugboards=plugboards
|
||||||
)
|
)
|
||||||
@ -1475,7 +1486,7 @@ class DeviceMixin(object): # {{{
|
|||||||
self.cover_to_thumbnail(open(book.cover, 'rb').read())
|
self.cover_to_thumbnail(open(book.cover, 'rb').read())
|
||||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
self.device_manager.sync_booklists(
|
self.device_manager.sync_booklists(
|
||||||
Dispatcher(self.metadata_synced), booklists,
|
FunctionDispatcher(self.metadata_synced), booklists,
|
||||||
plugboards)
|
plugboards)
|
||||||
return update_metadata
|
return update_metadata
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -11,10 +11,11 @@ from PyQt4.Qt import QDialog
|
|||||||
|
|
||||||
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
|
||||||
from calibre.gui2 import error_dialog, choose_dir
|
from calibre.gui2 import error_dialog, choose_dir
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding, iswindows
|
||||||
from calibre import isbytestring, patheq
|
from calibre import isbytestring, patheq
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2.wizard import move_library
|
from calibre.gui2.wizard import move_library
|
||||||
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
|
|
||||||
class ChooseLibrary(QDialog, Ui_Dialog):
|
class ChooseLibrary(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
@ -57,12 +58,20 @@ class ChooseLibrary(QDialog, Ui_Dialog):
|
|||||||
_('There is no existing calibre library at %s')%loc,
|
_('There is no existing calibre library at %s')%loc,
|
||||||
show=True)
|
show=True)
|
||||||
return False
|
return False
|
||||||
if ac in ('new', 'move') and not empty:
|
if ac in ('new', 'move'):
|
||||||
error_dialog(self, _('Not empty'),
|
if not empty:
|
||||||
|
error_dialog(self, _('Not empty'),
|
||||||
_('The folder %s is not empty. Please choose an empty'
|
_('The folder %s is not empty. Please choose an empty'
|
||||||
' folder')%loc,
|
' folder')%loc,
|
||||||
show=True)
|
show=True)
|
||||||
return False
|
return False
|
||||||
|
if (iswindows and len(loc) >
|
||||||
|
LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT):
|
||||||
|
error_dialog(self, _('Too long'),
|
||||||
|
_('Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT,
|
||||||
|
show=True)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -148,19 +148,21 @@ class ViewLog(QDialog): # {{{
|
|||||||
QApplication.clipboard().setText(txt)
|
QApplication.clipboard().setText(txt)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
_proceed_memory = []
|
_proceed_memory = []
|
||||||
|
|
||||||
class ProceedNotification(MessageBox): # {{{
|
class ProceedNotification(MessageBox): # {{{
|
||||||
|
|
||||||
def __init__(self, callback, payload, html_log, log_viewer_title, title, msg,
|
def __init__(self, callback, payload, html_log, log_viewer_title, title, msg,
|
||||||
det_msg='', show_copy_button=False, parent=None):
|
det_msg='', show_copy_button=False, parent=None,
|
||||||
|
cancel_callback=None):
|
||||||
'''
|
'''
|
||||||
A non modal popup that notifies the user that a background task has
|
A non modal popup that notifies the user that a background task has
|
||||||
been completed.
|
been completed.
|
||||||
|
|
||||||
:param callback: A callable that is called with payload if the user
|
:param callback: A callable that is called with payload if the user
|
||||||
asks to proceed. Note that this is always called in the GUI thread
|
asks to proceed. Note that this is always called in the GUI thread.
|
||||||
|
:param cancel_callback: A callable that is called with the payload if
|
||||||
|
the users asks not to proceed.
|
||||||
:param payload: Arbitrary object, passed to callback
|
:param payload: Arbitrary object, passed to callback
|
||||||
:param html_log: An HTML or plain text log
|
:param html_log: An HTML or plain text log
|
||||||
:param log_viewer_title: The title for the log viewer window
|
:param log_viewer_title: The title for the log viewer window
|
||||||
@ -181,7 +183,7 @@ class ProceedNotification(MessageBox): # {{{
|
|||||||
self.vlb.clicked.connect(self.show_log)
|
self.vlb.clicked.connect(self.show_log)
|
||||||
self.det_msg_toggle.setVisible(bool(det_msg))
|
self.det_msg_toggle.setVisible(bool(det_msg))
|
||||||
self.setModal(False)
|
self.setModal(False)
|
||||||
self.callback = callback
|
self.callback, self.cancel_callback = callback, cancel_callback
|
||||||
_proceed_memory.append(self)
|
_proceed_memory.append(self)
|
||||||
|
|
||||||
def show_log(self):
|
def show_log(self):
|
||||||
@ -192,15 +194,57 @@ class ProceedNotification(MessageBox): # {{{
|
|||||||
try:
|
try:
|
||||||
if result == self.Accepted:
|
if result == self.Accepted:
|
||||||
self.callback(self.payload)
|
self.callback(self.payload)
|
||||||
|
elif self.cancel_callback is not None:
|
||||||
|
self.cancel_callback(self.payload)
|
||||||
finally:
|
finally:
|
||||||
# Ensure this notification is garbage collected
|
# Ensure this notification is garbage collected
|
||||||
self.callback = None
|
self.callback = self.cancel_callback = None
|
||||||
self.setParent(None)
|
self.setParent(None)
|
||||||
self.finished.disconnect()
|
self.finished.disconnect()
|
||||||
self.vlb.clicked.disconnect()
|
self.vlb.clicked.disconnect()
|
||||||
_proceed_memory.remove(self)
|
_proceed_memory.remove(self)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
class ErrorNotification(MessageBox): # {{{
|
||||||
|
|
||||||
|
def __init__(self, html_log, log_viewer_title, title, msg,
|
||||||
|
det_msg='', show_copy_button=False, parent=None):
|
||||||
|
'''
|
||||||
|
A non modal popup that notifies the user that a background task has
|
||||||
|
errored.
|
||||||
|
|
||||||
|
:param html_log: An HTML or plain text log
|
||||||
|
:param log_viewer_title: The title for the log viewer window
|
||||||
|
:param title: The title for this popup
|
||||||
|
:param msg: The msg to display
|
||||||
|
:param det_msg: Detailed message
|
||||||
|
'''
|
||||||
|
MessageBox.__init__(self, MessageBox.ERROR, title, msg,
|
||||||
|
det_msg=det_msg, show_copy_button=show_copy_button,
|
||||||
|
parent=parent)
|
||||||
|
self.html_log = html_log
|
||||||
|
self.log_viewer_title = log_viewer_title
|
||||||
|
self.finished.connect(self.do_close, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole)
|
||||||
|
self.vlb.setIcon(QIcon(I('debug.png')))
|
||||||
|
self.vlb.clicked.connect(self.show_log)
|
||||||
|
self.det_msg_toggle.setVisible(bool(det_msg))
|
||||||
|
self.setModal(False)
|
||||||
|
_proceed_memory.append(self)
|
||||||
|
|
||||||
|
def show_log(self):
|
||||||
|
self.log_viewer = ViewLog(self.log_viewer_title, self.html_log,
|
||||||
|
parent=self)
|
||||||
|
|
||||||
|
def do_close(self, result):
|
||||||
|
# Ensure this notification is garbage collected
|
||||||
|
self.setParent(None)
|
||||||
|
self.finished.disconnect()
|
||||||
|
self.vlb.clicked.disconnect()
|
||||||
|
_proceed_memory.remove(self)
|
||||||
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
from calibre.gui2 import question_dialog
|
from calibre.gui2 import question_dialog
|
||||||
|
@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os, socket, time
|
import os, socket, time
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from threading import Thread
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
|
|
||||||
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
from calibre.utils.smtp import (compose_mail, sendmail, extract_email_address,
|
||||||
config as email_config
|
config as email_config)
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.customize.ui import available_input_formats, available_output_formats
|
from calibre.customize.ui import available_input_formats, available_output_formats
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
@ -22,9 +23,30 @@ from calibre.library.save_to_disk import get_components
|
|||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||||
|
|
||||||
|
class Worker(Thread):
|
||||||
|
|
||||||
|
def __init__(self, func, args):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.exception = self.tb = None
|
||||||
|
self.func, self.args = func, args
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
#time.sleep(1000)
|
||||||
|
try:
|
||||||
|
self.func(*self.args)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
self.exception = e
|
||||||
|
self.tb = traceback.format_exc()
|
||||||
|
finally:
|
||||||
|
self.func = self.args = None
|
||||||
|
|
||||||
|
|
||||||
class Sendmail(object):
|
class Sendmail(object):
|
||||||
|
|
||||||
MAX_RETRIES = 1
|
MAX_RETRIES = 1
|
||||||
|
TIMEOUT = 15 * 60 # seconds
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calculate_rate_limit()
|
self.calculate_rate_limit()
|
||||||
@ -42,22 +64,32 @@ class Sendmail(object):
|
|||||||
abort=None, notifications=None):
|
abort=None, notifications=None):
|
||||||
|
|
||||||
try_count = 0
|
try_count = 0
|
||||||
while try_count <= self.MAX_RETRIES:
|
while True:
|
||||||
if try_count > 0:
|
if try_count > 0:
|
||||||
log('\nRetrying in %d seconds...\n' %
|
log('\nRetrying in %d seconds...\n' %
|
||||||
self.rate_limit)
|
self.rate_limit)
|
||||||
try:
|
worker = Worker(self.sendmail,
|
||||||
self.sendmail(attachment, aname, to, subject, text, log)
|
(attachment, aname, to, subject, text, log))
|
||||||
try_count = self.MAX_RETRIES
|
worker.start()
|
||||||
log('Email successfully sent')
|
start_time = time.time()
|
||||||
except:
|
while worker.is_alive():
|
||||||
|
worker.join(0.2)
|
||||||
if abort.is_set():
|
if abort.is_set():
|
||||||
|
log('Sending aborted by user')
|
||||||
return
|
return
|
||||||
if try_count == self.MAX_RETRIES:
|
if time.time() - start_time > self.TIMEOUT:
|
||||||
raise
|
log('Sending timed out')
|
||||||
log.exception('\nSending failed...\n')
|
raise Exception(
|
||||||
|
'Sending email %r to %r timed out, aborting'% (subject,
|
||||||
|
to))
|
||||||
|
if worker.exception is None:
|
||||||
|
log('Email successfully sent')
|
||||||
|
return
|
||||||
|
log.error('\nSending failed...\n')
|
||||||
|
log.debug(worker.tb)
|
||||||
try_count += 1
|
try_count += 1
|
||||||
|
if try_count > self.MAX_RETRIES:
|
||||||
|
raise worker.exception
|
||||||
|
|
||||||
def sendmail(self, attachment, aname, to, subject, text, log):
|
def sendmail(self, attachment, aname, to, subject, text, log):
|
||||||
while time.time() - self.last_send_time <= self.rate_limit:
|
while time.time() - self.last_send_time <= self.rate_limit:
|
||||||
@ -67,8 +99,8 @@ class Sendmail(object):
|
|||||||
from_ = opts.from_
|
from_ = opts.from_
|
||||||
if not from_:
|
if not from_:
|
||||||
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||||
msg = compose_mail(from_, to, text, subject, open(attachment, 'rb'),
|
with lopen(attachment, 'rb') as f:
|
||||||
aname)
|
msg = compose_mail(from_, to, text, subject, f, aname)
|
||||||
efrom, eto = map(extract_email_address, (from_, to))
|
efrom, eto = map(extract_email_address, (from_, to))
|
||||||
eto = [eto]
|
eto = [eto]
|
||||||
sendmail(msg, efrom, eto, localhost=None,
|
sendmail(msg, efrom, eto, localhost=None,
|
||||||
@ -90,7 +122,7 @@ def send_mails(jobnames, callback, attachments, to_s, subjects,
|
|||||||
attachments, to_s, subjects, texts, attachment_names):
|
attachments, to_s, subjects, texts, attachment_names):
|
||||||
description = _('Email %s to %s') % (name, to)
|
description = _('Email %s to %s') % (name, to)
|
||||||
job = ThreadedJob('email', description, gui_sendmail, (attachment, aname, to,
|
job = ThreadedJob('email', description, gui_sendmail, (attachment, aname, to,
|
||||||
subject, text), {}, callback, killable=False)
|
subject, text), {}, callback)
|
||||||
job_manager.run_threaded_job(job)
|
job_manager.run_threaded_job(job)
|
||||||
|
|
||||||
|
|
||||||
|
@ -197,10 +197,12 @@ class JobManager(QAbstractTableModel): # {{{
|
|||||||
def row_to_job(self, row):
|
def row_to_job(self, row):
|
||||||
return self.jobs[row]
|
return self.jobs[row]
|
||||||
|
|
||||||
def has_device_jobs(self):
|
def has_device_jobs(self, queued_also=False):
|
||||||
for job in self.jobs:
|
for job in self.jobs:
|
||||||
if job.is_running and isinstance(job, DeviceJob):
|
if isinstance(job, DeviceJob):
|
||||||
return True
|
if job.duration is None: # Running or waiting
|
||||||
|
if (job.is_running or queued_also):
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_jobs(self):
|
def has_jobs(self):
|
||||||
@ -455,7 +457,7 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
|||||||
|
|
||||||
def kill_job(self, *args):
|
def kill_job(self, *args):
|
||||||
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop the selected job?')):
|
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop the selected job?')):
|
||||||
for index in self.jobs_view.selectedIndexes():
|
for index in self.jobs_view.selectionModel().selectedRows():
|
||||||
row = index.row()
|
row = index.row()
|
||||||
self.model.kill_job(row, self)
|
self.model.kill_job(row, self)
|
||||||
|
|
||||||
|
@ -1110,6 +1110,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
if self.last_search:
|
if self.last_search:
|
||||||
self.searched.emit(True)
|
self.searched.emit(True)
|
||||||
|
|
||||||
|
def research(self, reset=True):
|
||||||
|
self.search(self.last_search, reset)
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
descending = order != Qt.AscendingOrder
|
descending = order != Qt.AscendingOrder
|
||||||
@ -1171,6 +1173,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
self.custom_columns = {}
|
self.custom_columns = {}
|
||||||
self.db = db
|
self.db = db
|
||||||
self.map = list(range(0, len(db)))
|
self.map = list(range(0, len(db)))
|
||||||
|
self.research(reset=False)
|
||||||
|
self.resort()
|
||||||
|
|
||||||
def cover(self, row):
|
def cover(self, row):
|
||||||
item = self.db[self.map[row]]
|
item = self.db[self.map[row]]
|
||||||
@ -1319,8 +1323,6 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
||||||
'left')]
|
'left')]
|
||||||
return QVariant(ans)
|
return QVariant(ans)
|
||||||
|
|
||||||
|
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
|
@ -48,7 +48,7 @@ class BooksView(QTableView): # {{{
|
|||||||
files_dropped = pyqtSignal(object)
|
files_dropped = pyqtSignal(object)
|
||||||
add_column_signal = pyqtSignal()
|
add_column_signal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent, modelcls=BooksModel):
|
def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
|
||||||
QTableView.__init__(self, parent)
|
QTableView.__init__(self, parent)
|
||||||
|
|
||||||
self.setEditTriggers(self.EditKeyPressed)
|
self.setEditTriggers(self.EditKeyPressed)
|
||||||
@ -60,8 +60,12 @@ class BooksView(QTableView): # {{{
|
|||||||
elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
|
elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
|
||||||
# Must not enable single-click to edit, or the field will remain
|
# Must not enable single-click to edit, or the field will remain
|
||||||
# open in edit mode underneath the edit metadata dialog
|
# open in edit mode underneath the edit metadata dialog
|
||||||
self.doubleClicked.connect(
|
if use_edit_metadata_dialog:
|
||||||
partial(parent.iactions['Edit Metadata'].edit_metadata, checked=False))
|
self.doubleClicked.connect(
|
||||||
|
partial(parent.iactions['Edit Metadata'].edit_metadata,
|
||||||
|
checked=False))
|
||||||
|
else:
|
||||||
|
self.setEditTriggers(self.DoubleClicked|self.editTriggers())
|
||||||
|
|
||||||
self.drag_allowed = True
|
self.drag_allowed = True
|
||||||
self.setDragEnabled(True)
|
self.setDragEnabled(True)
|
||||||
@ -792,7 +796,8 @@ class BooksView(QTableView): # {{{
|
|||||||
class DeviceBooksView(BooksView): # {{{
|
class DeviceBooksView(BooksView): # {{{
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
BooksView.__init__(self, parent, DeviceBooksModel)
|
BooksView.__init__(self, parent, DeviceBooksModel,
|
||||||
|
use_edit_metadata_dialog=False)
|
||||||
self.can_add_columns = False
|
self.can_add_columns = False
|
||||||
self.columns_resized = False
|
self.columns_resized = False
|
||||||
self.resize_on_select = False
|
self.resize_on_select = False
|
||||||
|
@ -11,8 +11,8 @@ import textwrap, re, os
|
|||||||
|
|
||||||
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal, QMessageBox,
|
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal, QMessageBox,
|
||||||
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication,
|
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication,
|
||||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
|
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog,
|
||||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
|
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox)
|
||||||
|
|
||||||
from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
|
from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
|
||||||
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
||||||
@ -1061,10 +1061,68 @@ class IdentifiersEdit(QLineEdit): # {{{
|
|||||||
|
|
||||||
def paste_isbn(self):
|
def paste_isbn(self):
|
||||||
text = unicode(QApplication.clipboard().text()).strip()
|
text = unicode(QApplication.clipboard().text()).strip()
|
||||||
if text:
|
if not text or not check_isbn(text):
|
||||||
vals = self.current_val
|
d = ISBNDialog(self, text)
|
||||||
vals['isbn'] = text
|
if not d.exec_():
|
||||||
self.current_val = vals
|
return
|
||||||
|
text = d.text()
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
vals = self.current_val
|
||||||
|
vals['isbn'] = text
|
||||||
|
self.current_val = vals
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class ISBNDialog(QDialog) : # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent, txt):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
l = QGridLayout()
|
||||||
|
self.setLayout(l)
|
||||||
|
self.setWindowTitle(_('Invalid ISBN'))
|
||||||
|
w = QLabel(_('Enter an ISBN'))
|
||||||
|
l.addWidget(w, 0, 0, 1, 2)
|
||||||
|
w = QLabel(_('ISBN:'))
|
||||||
|
l.addWidget(w, 1, 0, 1, 1)
|
||||||
|
self.line_edit = w = QLineEdit();
|
||||||
|
w.setText(txt)
|
||||||
|
w.selectAll()
|
||||||
|
w.textChanged.connect(self.checkText)
|
||||||
|
l.addWidget(w, 1, 1, 1, 1)
|
||||||
|
w = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
|
||||||
|
l.addWidget(w, 2, 0, 1, 2)
|
||||||
|
w.accepted.connect(self.accept)
|
||||||
|
w.rejected.connect(self.reject)
|
||||||
|
self.checkText(self.text())
|
||||||
|
sz = self.sizeHint()
|
||||||
|
sz.setWidth(sz.width()+50)
|
||||||
|
self.resize(sz)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
isbn = unicode(self.line_edit.text())
|
||||||
|
if not check_isbn(isbn):
|
||||||
|
return error_dialog(self, _('Invalid ISBN'),
|
||||||
|
_('The ISBN you entered is not valid. Try again.'),
|
||||||
|
show=True)
|
||||||
|
QDialog.accept(self)
|
||||||
|
|
||||||
|
def checkText(self, txt):
|
||||||
|
isbn = unicode(txt)
|
||||||
|
if not isbn:
|
||||||
|
col = 'none'
|
||||||
|
extra = ''
|
||||||
|
elif check_isbn(isbn) is not None:
|
||||||
|
col = 'rgba(0,255,0,20%)'
|
||||||
|
extra = _('This ISBN number is valid')
|
||||||
|
else:
|
||||||
|
col = 'rgba(255,0,0,20%)'
|
||||||
|
extra = _('This ISBN number is invalid')
|
||||||
|
self.line_edit.setToolTip(extra)
|
||||||
|
self.line_edit.setStyleSheet('QLineEdit { background-color: %s }'%col)
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
return unicode(self.line_edit.text())
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
show=True)
|
show=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
def update_from_mi(self, mi, update_sorts=True):
|
def update_from_mi(self, mi, update_sorts=True, merge_tags=True):
|
||||||
if not mi.is_null('title'):
|
if not mi.is_null('title'):
|
||||||
self.title.current_val = mi.title
|
self.title.current_val = mi.title
|
||||||
if update_sorts:
|
if update_sorts:
|
||||||
@ -334,7 +334,11 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
if not mi.is_null('publisher'):
|
if not mi.is_null('publisher'):
|
||||||
self.publisher.current_val = mi.publisher
|
self.publisher.current_val = mi.publisher
|
||||||
if not mi.is_null('tags'):
|
if not mi.is_null('tags'):
|
||||||
self.tags.current_val = mi.tags
|
old_tags = self.tags.current_val
|
||||||
|
tags = mi.tags if mi.tags else []
|
||||||
|
if old_tags and merge_tags:
|
||||||
|
tags += old_tags
|
||||||
|
self.tags.current_val = tags
|
||||||
if not mi.is_null('identifiers'):
|
if not mi.is_null('identifiers'):
|
||||||
current = self.identifiers.current_val
|
current = self.identifiers.current_val
|
||||||
current.update(mi.identifiers)
|
current.update(mi.identifiers)
|
||||||
|
@ -462,11 +462,11 @@ class EditRules(QWidget): # {{{
|
|||||||
self.l = l = QGridLayout(self)
|
self.l = l = QGridLayout(self)
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
|
|
||||||
self.l1 = l1 = QLabel(_(
|
self.l1 = l1 = QLabel('<p>'+_(
|
||||||
'You can control the color of columns in the'
|
'You can control the color of columns in the'
|
||||||
' book list by creating "rules" that tell calibre'
|
' book list by creating "rules" that tell calibre'
|
||||||
' what color to use. Click the Add Rule button below'
|
' what color to use. Click the Add Rule button below'
|
||||||
' to get started. You can change an existing rule by double'
|
' to get started.<p>You can <b>change an existing rule</b> by double'
|
||||||
' clicking it.'))
|
' clicking it.'))
|
||||||
l1.setWordWrap(True)
|
l1.setWordWrap(True)
|
||||||
l.addWidget(l1, 0, 0, 1, 2)
|
l.addWidget(l1, 0, 0, 1, 2)
|
||||||
|
@ -131,16 +131,34 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
|
|
||||||
# Amazon has two results pages.
|
# Amazon has two results pages.
|
||||||
is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
|
is_shot = doc.xpath('boolean(//div[@id="shotgunMainResults"])')
|
||||||
# Horizontal grid of books.
|
# Horizontal grid of books. Search "Paolo Bacigalupi"
|
||||||
if is_shot:
|
if is_shot:
|
||||||
data_xpath = '//div[contains(@class, "result")]'
|
data_xpath = '//div[contains(@class, "result")]'
|
||||||
format_xpath = './/div[@class="productTitle"]/text()'
|
format_xpath = './/div[@class="productTitle"]//text()'
|
||||||
|
asin_xpath = './/div[@class="productTitle"]//a'
|
||||||
cover_xpath = './/div[@class="productTitle"]//img/@src'
|
cover_xpath = './/div[@class="productTitle"]//img/@src'
|
||||||
|
title_xpath = './/div[@class="productTitle"]/a//text()'
|
||||||
|
price_xpath = './/div[@class="newPrice"]/span/text()'
|
||||||
# Vertical list of books.
|
# Vertical list of books.
|
||||||
else:
|
else:
|
||||||
data_xpath = '//div[@class="productData"]'
|
# New style list. Search "Paolo Bacigalupi"
|
||||||
format_xpath = './/span[@class="format"]/text()'
|
if doc.xpath('boolean(//div[@class="image"])'):
|
||||||
cover_xpath = '../div[@class="productImage"]/a/img/@src'
|
data_xpath = '//div[contains(@class, "results")]//div[contains(@class, "result")]'
|
||||||
|
format_xpath = './/span[@class="binding"]//text()'
|
||||||
|
asin_xpath = './/div[@class="image"]/a[1]'
|
||||||
|
cover_xpath = './/img[@class="productImage"]/@src'
|
||||||
|
title_xpath = './/a[@class="title"]/text()'
|
||||||
|
price_xpath = './/span[@class="price"]/text()'
|
||||||
|
# Old style list. Search "martin"
|
||||||
|
else:
|
||||||
|
data_xpath = '//div[contains(@class, "result")]'
|
||||||
|
format_xpath = './/span[@class="format"]//text()'
|
||||||
|
asin_xpath = './/div[@class="productImage"]/a[1]'
|
||||||
|
cover_xpath = './/div[@class="productImage"]//img/@src'
|
||||||
|
title_xpath = './/div[@class="productTitle"]/a/text()'
|
||||||
|
price_xpath = './/div[@class="newPrice"]//span//text()'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for data in doc.xpath(data_xpath):
|
for data in doc.xpath(data_xpath):
|
||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
@ -157,7 +175,7 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
# We must have an asin otherwise we can't easily reference the
|
# We must have an asin otherwise we can't easily reference the
|
||||||
# book later.
|
# book later.
|
||||||
asin_href = None
|
asin_href = None
|
||||||
asin_a = data.xpath('.//div[@class="productTitle"]/a[1]')
|
asin_a = data.xpath(asin_xpath)
|
||||||
if asin_a:
|
if asin_a:
|
||||||
asin_href = asin_a[0].get('href', '')
|
asin_href = asin_a[0].get('href', '')
|
||||||
m = re.search(r'/dp/(?P<asin>.+?)(/|$)', asin_href)
|
m = re.search(r'/dp/(?P<asin>.+?)(/|$)', asin_href)
|
||||||
@ -170,14 +188,14 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
|
|
||||||
cover_url = ''.join(data.xpath(cover_xpath))
|
cover_url = ''.join(data.xpath(cover_xpath))
|
||||||
|
|
||||||
title = ''.join(data.xpath('.//div[@class="productTitle"]/a/text()'))
|
title = ''.join(data.xpath(title_xpath))
|
||||||
price = ''.join(data.xpath('.//div[@class="newPrice"]/span/text()'))
|
price = ''.join(data.xpath(price_xpath))
|
||||||
|
|
||||||
if is_shot:
|
if is_shot:
|
||||||
author = format.split(' by ')[-1]
|
author = format.split(' by ')[-1]
|
||||||
else:
|
else:
|
||||||
author = ''.join(data.xpath('.//div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
author = ''.join(data.xpath('.//span[@class="ptBrand"]/text()'))
|
||||||
author = author.split(' by ')[-1]
|
author = author.split('by ')[-1]
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import urllib
|
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
@ -47,26 +46,26 @@ class BNStore(BasicStoreConfig, StorePlugin):
|
|||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
url = 'http://productsearch.barnesandnoble.com/search/results.aspx?STORE=EBOOK&SZE=%s&WRD=' % max_results
|
query = query.replace(' ', '-')
|
||||||
url += urllib.quote_plus(query)
|
url = 'http://www.barnesandnoble.com/s/%s?store=ebook&sze=%s' % (query, max_results)
|
||||||
|
|
||||||
br = browser()
|
br = browser()
|
||||||
|
|
||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
for data in doc.xpath('//ul[contains(@class, "wgt-search-results-display")]/li[contains(@class, "search-result-item") and contains(@class, "nook-result-item")]'):
|
for data in doc.xpath('//ul[contains(@class, "result-set")]/li[contains(@class, "result")]'):
|
||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
id = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/@href'))
|
id = ''.join(data.xpath('.//div[contains(@class, "image")]/a/@href'))
|
||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
cover_url = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/img/@src'))
|
cover_url = ''.join(data.xpath('.//div[contains(@class, "image")]//img/@src'))
|
||||||
|
|
||||||
title = ''.join(data.xpath('.//span[@class="product-title"]/a/text()'))
|
title = ''.join(data.xpath('.//p[@class="title"]//span[@class="name"]/text()'))
|
||||||
author = ', '.join(data.xpath('.//span[@class="contributers-line"]/a/text()'))
|
author = ', '.join(data.xpath('.//ul[@class="contributors"]//li[position()>1]//a/text()'))
|
||||||
price = ''.join(data.xpath('.//span[contains(@class, "onlinePriceValue2")]/text()'))
|
price = ''.join(data.xpath('.//table[@class="displayed-formats"]//a[@class="subtle"]/text()'))
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ class BNStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.cover_url = cover_url
|
s.cover_url = cover_url
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price
|
s.price = price.strip()
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
s.drm = SearchResult.DRM_UNKNOWN
|
s.drm = SearchResult.DRM_UNKNOWN
|
||||||
s.formats = 'Nook'
|
s.formats = 'Nook'
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import (QWidget, QIcon, QDialog)
|
from PyQt4.Qt import (QWidget, QIcon, QDialog, QComboBox)
|
||||||
|
|
||||||
from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog
|
from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog
|
||||||
from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form
|
from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form
|
||||||
@ -18,6 +18,8 @@ class StoreChooserWidget(QWidget, Ui_Form):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.query.initialize('store_config_chooser_query')
|
self.query.initialize('store_config_chooser_query')
|
||||||
|
self.query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.query.setMinimumContentsLength(25)
|
||||||
|
|
||||||
self.adv_search_builder.setIcon(QIcon(I('search.png')))
|
self.adv_search_builder.setIcon(QIcon(I('search.png')))
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ This is a list of stores that objected, declined
|
|||||||
or asked not to be included in the store integration.
|
or asked not to be included in the store integration.
|
||||||
|
|
||||||
* Borders (http://www.borders.com/)
|
* Borders (http://www.borders.com/)
|
||||||
* WH Smith (http://www.whsmith.co.uk/)
|
* Indigo (http://www.chapters.indigo.ca/)
|
||||||
Refused to permit signing up for the affiliate program
|
|
||||||
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
|
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
|
||||||
No reply with two attempts over 2 weeks
|
No reply with two attempts over 2 weeks
|
||||||
|
* WH Smith (http://www.whsmith.co.uk/)
|
||||||
|
Refused to permit signing up for the affiliate program
|
@ -7,7 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.Qt import (Qt, QDialog, QIcon)
|
from PyQt4.Qt import (Qt, QDialog, QIcon, QComboBox)
|
||||||
|
|
||||||
from calibre.gui2.store.mobileread.adv_search_builder import AdvSearchBuilderDialog
|
from calibre.gui2.store.mobileread.adv_search_builder import AdvSearchBuilderDialog
|
||||||
from calibre.gui2.store.mobileread.models import BooksModel
|
from calibre.gui2.store.mobileread.models import BooksModel
|
||||||
@ -21,6 +21,8 @@ class MobileReadStoreDialog(QDialog, Ui_Dialog):
|
|||||||
|
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.search_query.initialize('store_mobileread_search')
|
self.search_query.initialize('store_mobileread_search')
|
||||||
|
self.search_query.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.search_query.setMinimumContentsLength(25)
|
||||||
|
|
||||||
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class NextoStore(BasicStoreConfig, StorePlugin):
|
|||||||
author = ''
|
author = ''
|
||||||
with closing(br.open('http://www.nexto.pl/' + id.strip(), timeout=timeout/4)) as nf:
|
with closing(br.open('http://www.nexto.pl/' + id.strip(), timeout=timeout/4)) as nf:
|
||||||
idata = html.fromstring(nf.read())
|
idata = html.fromstring(nf.read())
|
||||||
author = ''.join(idata.xpath('//div[@class="basic_data"]/p[1]/b/a/text()'))
|
author = ', '.join(idata.xpath('//div[@class="basic_data"]/p[1]/b/a/text()'))
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import re
|
|||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
|
from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
|
||||||
QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout)
|
QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout,
|
||||||
|
QComboBox)
|
||||||
|
|
||||||
from calibre.gui2 import JSONConfig, info_dialog
|
from calibre.gui2 import JSONConfig, info_dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
@ -57,6 +58,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
|
|
||||||
# Set the search query
|
# Set the search query
|
||||||
self.search_edit.setText(query)
|
self.search_edit.setText(query)
|
||||||
|
self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.search_edit.setMinimumContentsLength(25)
|
||||||
|
|
||||||
# Create and add the progress indicator
|
# Create and add the progress indicator
|
||||||
self.pi = ProgressIndicator(self, 24)
|
self.pi = ProgressIndicator(self, 24)
|
||||||
@ -82,6 +85,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
def setup_store_checks(self):
|
def setup_store_checks(self):
|
||||||
|
first_run = self.config.get('first_run', True)
|
||||||
|
|
||||||
# Add check boxes for each store so the user
|
# Add check boxes for each store so the user
|
||||||
# can disable searching specific stores on a
|
# can disable searching specific stores on a
|
||||||
# per search basis.
|
# per search basis.
|
||||||
@ -98,7 +103,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
icon = QIcon(I('donate.png'))
|
icon = QIcon(I('donate.png'))
|
||||||
for i, x in enumerate(sorted(self.gui.istores.keys(), key=lambda x: x.lower())):
|
for i, x in enumerate(sorted(self.gui.istores.keys(), key=lambda x: x.lower())):
|
||||||
cbox = QCheckBox(x)
|
cbox = QCheckBox(x)
|
||||||
cbox.setChecked(existing.get(x, False))
|
cbox.setChecked(existing.get(x, first_run))
|
||||||
store_list_layout.addWidget(cbox, i, 0, 1, 1)
|
store_list_layout.addWidget(cbox, i, 0, 1, 1)
|
||||||
if self.gui.istores[x].base_plugin.affiliate:
|
if self.gui.istores[x].base_plugin.affiliate:
|
||||||
iw = QLabel(self)
|
iw = QLabel(self)
|
||||||
@ -109,6 +114,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
store_list_layout.setRowStretch(store_list_layout.rowCount(), 10)
|
store_list_layout.setRowStretch(store_list_layout.rowCount(), 10)
|
||||||
self.store_list.setWidget(stores_check_widget)
|
self.store_list.setWidget(stores_check_widget)
|
||||||
|
|
||||||
|
self.config['first_run'] = False
|
||||||
|
|
||||||
def build_adv_search(self):
|
def build_adv_search(self):
|
||||||
adv = AdvSearchBuilderDialog(self)
|
adv = AdvSearchBuilderDialog(self)
|
||||||
if adv.exec_() == QDialog.Accepted:
|
if adv.exec_() == QDialog.Accepted:
|
||||||
|
@ -6,15 +6,15 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>584</width>
|
<width>872</width>
|
||||||
<height>533</height>
|
<height>610</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Get Books</string>
|
<string>Get Books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
|
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeGripEnabled">
|
<property name="sizeGripEnabled">
|
||||||
@ -82,8 +82,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>125</width>
|
<width>173</width>
|
||||||
<height>127</height>
|
<height>106</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -255,7 +255,7 @@
|
|||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../../resources/images.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
|
80
src/calibre/gui2/store/zixo_plugin.py
Normal file
80
src/calibre/gui2/store/zixo_plugin.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, Tomasz Długosz <tomek3d@gmail.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from PyQt4.Qt import QUrl
|
||||||
|
|
||||||
|
from calibre import browser, url_slash_cleaner
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
from calibre.gui2.store import StorePlugin
|
||||||
|
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||||
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
|
||||||
|
class ZixoStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
|
|
||||||
|
url = 'http://zixo.pl/e_ksiazki/start/'
|
||||||
|
|
||||||
|
if external or self.config.get('open_external', False):
|
||||||
|
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||||
|
else:
|
||||||
|
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||||
|
d.setWindowTitle(self.name)
|
||||||
|
d.set_tags(self.config.get('tags', ''))
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def search(self, query, max_results=10, timeout=60):
|
||||||
|
url = 'http://zixo.pl/wyszukiwarka/?search=' + urllib.quote(query.encode('utf-8')) + '&product_type=0'
|
||||||
|
|
||||||
|
br = browser()
|
||||||
|
|
||||||
|
counter = max_results
|
||||||
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
|
doc = html.fromstring(f.read())
|
||||||
|
for data in doc.xpath('//div[@class="productInline"]'):
|
||||||
|
if counter <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
id = ''.join(data.xpath('.//a[@class="productThumb"]/@href'))
|
||||||
|
if not id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cover_url = ''.join(data.xpath('.//a[@class="productThumb"]/img/@src'))
|
||||||
|
title = ''.join(data.xpath('.//a[@class="title"]/text()'))
|
||||||
|
author = ''.join(data.xpath('.//div[@class="productDescription"]/span[1]/a/text()'))
|
||||||
|
price = ''.join(data.xpath('.//div[@class="priceList"]/span/text()'))
|
||||||
|
price = re.sub('\.', ',', price)
|
||||||
|
|
||||||
|
counter -= 1
|
||||||
|
|
||||||
|
s = SearchResult()
|
||||||
|
s.cover_url = cover_url
|
||||||
|
s.title = title.strip()
|
||||||
|
s.author = author.strip()
|
||||||
|
s.price = price
|
||||||
|
s.detail_item = 'http://zixo.pl' + id.strip()
|
||||||
|
s.drm = SearchResult.DRM_LOCKED
|
||||||
|
|
||||||
|
yield s
|
||||||
|
|
||||||
|
def get_details(self, search_result, timeout):
|
||||||
|
br = browser()
|
||||||
|
with closing(br.open(search_result.detail_item, timeout=timeout)) as nf:
|
||||||
|
idata = html.fromstring(nf.read())
|
||||||
|
formats = ''.join(idata.xpath('//ul[@class="prop"]/li[3]/text()'))
|
||||||
|
formats = re.sub(r'\(.*\)', '', formats)
|
||||||
|
formats = re.sub('Zixo Reader', 'ZIXO', formats)
|
||||||
|
search_result.formats = formats
|
||||||
|
return True
|
@ -610,7 +610,7 @@ class TagTreeItem(object): # {{{
|
|||||||
self.temporary = temporary
|
self.temporary = temporary
|
||||||
self.tag = Tag(data, category=category_key,
|
self.tag = Tag(data, category=category_key,
|
||||||
is_editable=category_key not in ['news', 'search', 'identifiers'],
|
is_editable=category_key not in ['news', 'search', 'identifiers'],
|
||||||
is_searchable=category_key not in ['news', 'search'])
|
is_searchable=category_key not in ['search'])
|
||||||
|
|
||||||
elif self.type == self.TAG:
|
elif self.type == self.TAG:
|
||||||
self.icon_state_map[0] = QVariant(data.icon)
|
self.icon_state_map[0] = QVariant(data.icon)
|
||||||
@ -1642,7 +1642,13 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
for node in self.category_nodes:
|
for node in self.category_nodes:
|
||||||
if node.tag.state:
|
if node.tag.state:
|
||||||
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
|
if node.category_key == "news":
|
||||||
|
if node_searches[node.tag.state] == 'true':
|
||||||
|
ans.append('tags:=news')
|
||||||
|
else:
|
||||||
|
ans.append('( not tags:=news )')
|
||||||
|
else:
|
||||||
|
ans.append('%s:%s'%(node.category_key, node_searches[node.tag.state]))
|
||||||
|
|
||||||
key = node.category_key
|
key = node.category_key
|
||||||
for tag_item in node.child_tags():
|
for tag_item in node.child_tags():
|
||||||
|
@ -8,10 +8,11 @@ import os, math, re, glob, sys
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QSize, QSizePolicy, QUrl, SIGNAL, Qt, QTimer, \
|
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, QTimer,
|
||||||
QPainter, QPalette, QBrush, QFontDatabase, QDialog, \
|
QPainter, QPalette, QBrush, QFontDatabase, QDialog,
|
||||||
QColor, QPoint, QImage, QRegion, QVariant, QIcon, \
|
QColor, QPoint, QImage, QRegion, QVariant, QIcon,
|
||||||
QFont, pyqtSignature, QAction, QByteArray, QMenu
|
QFont, pyqtSignature, QAction, QByteArray, QMenu,
|
||||||
|
pyqtSignal)
|
||||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
||||||
|
|
||||||
from calibre.utils.config import Config, StringConfig
|
from calibre.utils.config import Config, StringConfig
|
||||||
@ -496,6 +497,7 @@ class EntityDeclarationProcessor(object): # {{{
|
|||||||
|
|
||||||
class DocumentView(QWebView): # {{{
|
class DocumentView(QWebView): # {{{
|
||||||
|
|
||||||
|
magnification_changed = pyqtSignal(object)
|
||||||
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
@ -908,15 +910,22 @@ class DocumentView(QWebView): # {{{
|
|||||||
if notify and self.manager is not None and self.document.ypos != old_pos:
|
if notify and self.manager is not None and self.document.ypos != old_pos:
|
||||||
self.manager.scrolled(self.scroll_fraction)
|
self.manager.scrolled(self.scroll_fraction)
|
||||||
|
|
||||||
|
@dynamic_property
|
||||||
def multiplier(self):
|
def multiplier(self):
|
||||||
return self.document.mainFrame().textSizeMultiplier()
|
def fget(self):
|
||||||
|
return self.document.mainFrame().textSizeMultiplier()
|
||||||
|
def fset(self, val):
|
||||||
|
self.document.mainFrame().setTextSizeMultiplier(val)
|
||||||
|
self.magnification_changed.emit(val)
|
||||||
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
def magnify_fonts(self):
|
def magnify_fonts(self):
|
||||||
self.document.mainFrame().setTextSizeMultiplier(self.multiplier()+0.2)
|
self.multiplier += 0.2
|
||||||
return self.document.scroll_fraction
|
return self.document.scroll_fraction
|
||||||
|
|
||||||
def shrink_fonts(self):
|
def shrink_fonts(self):
|
||||||
self.document.mainFrame().setTextSizeMultiplier(max(self.multiplier()-0.2, 0))
|
if self.multiplier >= 0.2:
|
||||||
|
self.multiplier -= 0.2
|
||||||
return self.document.scroll_fraction
|
return self.document.scroll_fraction
|
||||||
|
|
||||||
def changeEvent(self, event):
|
def changeEvent(self, event):
|
||||||
|
@ -175,6 +175,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
def __init__(self, pathtoebook=None, debug_javascript=False):
|
def __init__(self, pathtoebook=None, debug_javascript=False):
|
||||||
MainWindow.__init__(self, None)
|
MainWindow.__init__(self, None)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
self.view.magnification_changed.connect(self.magnification_changed)
|
||||||
self.show_toc_on_open = False
|
self.show_toc_on_open = False
|
||||||
self.current_book_has_toc = False
|
self.current_book_has_toc = False
|
||||||
self.base_window_title = unicode(self.windowTitle())
|
self.base_window_title = unicode(self.windowTitle())
|
||||||
@ -345,6 +346,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
if self.toc.isVisible():
|
if self.toc.isVisible():
|
||||||
vprefs.set('viewer_splitter_state',
|
vprefs.set('viewer_splitter_state',
|
||||||
bytearray(self.splitter.saveState()))
|
bytearray(self.splitter.saveState()))
|
||||||
|
vprefs['multiplier'] = self.view.multiplier
|
||||||
|
|
||||||
def restore_state(self):
|
def restore_state(self):
|
||||||
state = vprefs.get('viewer_toolbar_state', None)
|
state = vprefs.get('viewer_toolbar_state', None)
|
||||||
@ -354,6 +356,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.restoreState(state, self.STATE_VERSION)
|
self.restoreState(state, self.STATE_VERSION)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
mult = vprefs.get('multiplier', None)
|
||||||
|
if mult:
|
||||||
|
self.view.multiplier = mult
|
||||||
|
|
||||||
|
|
||||||
def lookup(self, word):
|
def lookup(self, word):
|
||||||
@ -476,16 +481,22 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
|
|
||||||
def font_size_larger(self, checked):
|
def font_size_larger(self, checked):
|
||||||
frac = self.view.magnify_fonts()
|
frac = self.view.magnify_fonts()
|
||||||
self.action_font_size_larger.setEnabled(self.view.multiplier() < 3)
|
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
|
||||||
self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2)
|
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)
|
||||||
self.set_page_number(frac)
|
self.set_page_number(frac)
|
||||||
|
|
||||||
def font_size_smaller(self, checked):
|
def font_size_smaller(self, checked):
|
||||||
frac = self.view.shrink_fonts()
|
frac = self.view.shrink_fonts()
|
||||||
self.action_font_size_larger.setEnabled(self.view.multiplier() < 3)
|
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
|
||||||
self.action_font_size_smaller.setEnabled(self.view.multiplier() > 0.2)
|
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)
|
||||||
self.set_page_number(frac)
|
self.set_page_number(frac)
|
||||||
|
|
||||||
|
def magnification_changed(self, val):
|
||||||
|
tt = _('Make font size %s\nCurrent magnification: %.1f')
|
||||||
|
self.action_font_size_larger.setToolTip(
|
||||||
|
tt %(_('larger'), val))
|
||||||
|
self.action_font_size_smaller.setToolTip(
|
||||||
|
tt %(_('smaller'), val))
|
||||||
|
|
||||||
def find(self, text, repeat=False, backwards=False):
|
def find(self, text, repeat=False, backwards=False):
|
||||||
if not text:
|
if not text:
|
||||||
|
@ -16,7 +16,7 @@ from PyQt4.Qt import QWizard, QWizardPage, QPixmap, Qt, QAbstractListModel, \
|
|||||||
from calibre import __appname__, patheq
|
from calibre import __appname__, patheq
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.move import MoveLibrary
|
from calibre.library.move import MoveLibrary
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding, iswindows
|
||||||
from calibre.gui2.wizard.send_email import smtp_prefs
|
from calibre.gui2.wizard.send_email import smtp_prefs
|
||||||
from calibre.gui2.wizard.device_ui import Ui_WizardPage as DeviceUI
|
from calibre.gui2.wizard.device_ui import Ui_WizardPage as DeviceUI
|
||||||
from calibre.gui2.wizard.library_ui import Ui_WizardPage as LibraryUI
|
from calibre.gui2.wizard.library_ui import Ui_WizardPage as LibraryUI
|
||||||
@ -656,6 +656,13 @@ class LibraryPage(QWizardPage, LibraryUI):
|
|||||||
x = choose_dir(self, 'database location dialog',
|
x = choose_dir(self, 'database location dialog',
|
||||||
_('Select location for books'))
|
_('Select location for books'))
|
||||||
if x:
|
if x:
|
||||||
|
if (iswindows and len(x) >
|
||||||
|
LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT):
|
||||||
|
return error_dialog(self, _('Too long'),
|
||||||
|
_('Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%LibraryDatabase2.WINDOWS_LIBRARY_PATH_LIMIT,
|
||||||
|
show=True)
|
||||||
|
|
||||||
if self.is_library_dir_suitable(x):
|
if self.is_library_dir_suitable(x):
|
||||||
self.location.setText(x)
|
self.location.setText(x)
|
||||||
else:
|
else:
|
||||||
|
@ -26,7 +26,7 @@ from calibre.library.custom_columns import CustomColumns
|
|||||||
from calibre.library.sqlite import connect, IntegrityError
|
from calibre.library.sqlite import connect, IntegrityError
|
||||||
from calibre.library.prefs import DBPrefs
|
from calibre.library.prefs import DBPrefs
|
||||||
from calibre.ebooks.metadata.book.base import Metadata
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
|
from calibre.constants import preferred_encoding, iswindows, filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
from calibre import isbytestring
|
from calibre import isbytestring
|
||||||
@ -82,6 +82,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
An ebook metadata database that stores references to ebook files on disk.
|
An ebook metadata database that stores references to ebook files on disk.
|
||||||
'''
|
'''
|
||||||
PATH_LIMIT = 40 if 'win32' in sys.platform else 100
|
PATH_LIMIT = 40 if 'win32' in sys.platform else 100
|
||||||
|
WINDOWS_LIBRARY_PATH_LIMIT = 75
|
||||||
|
|
||||||
@dynamic_property
|
@dynamic_property
|
||||||
def user_version(self):
|
def user_version(self):
|
||||||
@ -122,9 +123,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return property(doc=doc, fget=fget, fset=fset)
|
return property(doc=doc, fget=fget, fset=fset)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if 'win32' in sys.platform and len(self.library_path) + 4*self.PATH_LIMIT + 10 > 259:
|
if iswindows and len(self.library_path) + 4*self.PATH_LIMIT + 10 > 259:
|
||||||
raise ValueError('Path to library too long. Must be less than %d characters.'%(259-4*self.PATH_LIMIT-10))
|
raise ValueError(_(
|
||||||
|
'Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%(259-4*self.PATH_LIMIT-10))
|
||||||
exists = os.path.exists(self.dbpath)
|
exists = os.path.exists(self.dbpath)
|
||||||
|
if not exists:
|
||||||
|
# Be more strict when creating new libraries as the old calculation
|
||||||
|
# allowed for max path lengths of 265 chars.
|
||||||
|
if (iswindows and len(self.library_path) >
|
||||||
|
self.WINDOWS_LIBRARY_PATH_LIMIT):
|
||||||
|
raise ValueError(_(
|
||||||
|
'Path to library too long. Must be less than'
|
||||||
|
' %d characters.')%self.WINDOWS_LIBRARY_PATH_LIMIT)
|
||||||
|
|
||||||
self.conn = connect(self.dbpath, self.row_factory)
|
self.conn = connect(self.dbpath, self.row_factory)
|
||||||
if exists and self.user_version == 0:
|
if exists and self.user_version == 0:
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
@ -176,8 +188,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
apply_default_prefs = not os.path.exists(self.dbpath)
|
apply_default_prefs = not os.path.exists(self.dbpath)
|
||||||
self.connect()
|
self.connect()
|
||||||
|
|
||||||
self.is_case_sensitive = not iswindows and not isosx and \
|
self.is_case_sensitive = (not iswindows and
|
||||||
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB'))
|
not os.path.exists(self.dbpath.replace('metadata.db',
|
||||||
|
'MeTAdAtA.dB')))
|
||||||
SchemaUpgrade.__init__(self)
|
SchemaUpgrade.__init__(self)
|
||||||
# Guarantee that the library_id is set
|
# Guarantee that the library_id is set
|
||||||
self.library_id
|
self.library_id
|
||||||
@ -594,7 +607,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
f = self.format(id, format, index_is_id=True, as_file=True)
|
f = self.format(id, format, index_is_id=True, as_file=True)
|
||||||
if f is None:
|
if f is None:
|
||||||
continue
|
continue
|
||||||
with tempfile.SpooledTemporaryFile(max_size=100*(1024**2)) as stream:
|
with tempfile.SpooledTemporaryFile(max_size=30*(1024**2)) as stream:
|
||||||
with f:
|
with f:
|
||||||
shutil.copyfileobj(f, stream)
|
shutil.copyfileobj(f, stream)
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
@ -770,7 +770,8 @@ class BrowseServer(object):
|
|||||||
summs.append(self.browse_summary_template.format(**args))
|
summs.append(self.browse_summary_template.format(**args))
|
||||||
|
|
||||||
|
|
||||||
return json.dumps('\n'.join(summs), ensure_ascii=False)
|
raw = json.dumps('\n'.join(summs), ensure_ascii=False)
|
||||||
|
return raw
|
||||||
|
|
||||||
def browse_render_details(self, id_):
|
def browse_render_details(self, id_):
|
||||||
try:
|
try:
|
||||||
|
@ -189,7 +189,7 @@ Extra CSS
|
|||||||
|
|
||||||
This option allows you to specify arbitrary CSS that will be applied to all HTML files in the
|
This option allows you to specify arbitrary CSS that will be applied to all HTML files in the
|
||||||
input. This CSS is applied with very high priority and so should override most CSS present in
|
input. This CSS is applied with very high priority and so should override most CSS present in
|
||||||
the input document itself. You can use this setting to fine tune the presentation/layout of your
|
the **input document** itself. You can use this setting to fine tune the presentation/layout of your
|
||||||
document. For example, if you want all paragraphs of class `endnote` to be right aligned, just
|
document. For example, if you want all paragraphs of class `endnote` to be right aligned, just
|
||||||
add::
|
add::
|
||||||
|
|
||||||
@ -200,7 +200,8 @@ or if you want to change the indentation of all paragraphs::
|
|||||||
p { text-indent: 5mm; }
|
p { text-indent: 5mm; }
|
||||||
|
|
||||||
:guilabel:`Extra CSS` is a very powerful option, but you do need an understanding of how CSS works
|
:guilabel:`Extra CSS` is a very powerful option, but you do need an understanding of how CSS works
|
||||||
to use it to its full potential.
|
to use it to its full potential. You can use the debug pipeline option described above to see what
|
||||||
|
CSS is present in your input document.
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
@ -319,7 +319,7 @@ but it requires that the Kindle be rebooted *every time* it is disconnected from
|
|||||||
changes to the collections to be recognized. As such, it is unlikely that
|
changes to the collections to be recognized. As such, it is unlikely that
|
||||||
any |app| developers will ever feel motivated enough to support it. There is however, a |app| plugin
|
any |app| developers will ever feel motivated enough to support it. There is however, a |app| plugin
|
||||||
that allows you to create collections on your Kindle from the |app| metadata. It is available
|
that allows you to create collections on your Kindle from the |app| metadata. It is available
|
||||||
`here <http://www.mobileread.com/forums/showthread.php?t=118635>`_.
|
`from here <http://www.mobileread.com/forums/showthread.php?t=118635>`_.
|
||||||
|
|
||||||
Library Management
|
Library Management
|
||||||
------------------
|
------------------
|
||||||
@ -570,7 +570,7 @@ For many reasons:
|
|||||||
* *There is no need to update every week*. If you are happy with how |app| works turn off the update notification and be on your merry way. Check back to see if you want to update once a year or so.
|
* *There is no need to update every week*. If you are happy with how |app| works turn off the update notification and be on your merry way. Check back to see if you want to update once a year or so.
|
||||||
* Pre downloading the updates for all users in the background would mean require about 80TB of bandwidth *every week*. That costs thousands of dollars a month. And |app| is currently growing at 300,000 new users every month.
|
* Pre downloading the updates for all users in the background would mean require about 80TB of bandwidth *every week*. That costs thousands of dollars a month. And |app| is currently growing at 300,000 new users every month.
|
||||||
* If I implement a dialog that downloads the update and launches it, instead of going to the website as it does now, that would save the most ardent |app| updater, *at most five clicks a week*. There are far higher priority things to do in |app| development.
|
* If I implement a dialog that downloads the update and launches it, instead of going to the website as it does now, that would save the most ardent |app| updater, *at most five clicks a week*. There are far higher priority things to do in |app| development.
|
||||||
* If you really, really hate downloading |app| every week but still want to be upto the latest, I encourage you to run from source, which makes updating trivial. Instructions are :ref:`here <develop>`.
|
* If you really, really hate downloading |app| every week but still want to be up to the latest, I encourage you to run from source, which makes updating trivial. Instructions are :ref:`available here <develop>`.
|
||||||
|
|
||||||
How is |app| licensed?
|
How is |app| licensed?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@ -579,7 +579,7 @@ How is |app| licensed?
|
|||||||
How do I run calibre from my USB stick?
|
How do I run calibre from my USB stick?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
A portable version of calibre is available at: `portableapps.com <http://portableapps.com/node/20518>`_. However, this is usually out of date. You can also setup your own portable calibre install by following :ref:`these instructions <portablecalibre>`.
|
A portable version of calibre is available `here <http://calibre-ebook.com/download_portable>`_.
|
||||||
|
|
||||||
How do I run parts of |app| like news download and the content server on my own linux server?
|
How do I run parts of |app| like news download and the content server on my own linux server?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -326,7 +326,8 @@ Two other kinds of searches are available: equality search and search using regu
|
|||||||
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
||||||
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
|
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
|
||||||
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
||||||
be used. Regular expression searches are contains searches unless the expression contains anchors.
|
be used. Note that backslashes use to escape special characters in reqular expressions must be doubled, because single backslashes will be removed during query parsing. For example, to match a literal parenthesis, you must enter ``\\(``. Regular expression searches are contains searches unless the expression contains anchors.
|
||||||
|
|
||||||
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
||||||
|
|
||||||
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
|
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
|
|
||||||
.. include:: global.rst
|
|
||||||
|
|
||||||
.. _portablecalibre:
|
|
||||||
|
|
||||||
Creating your own portable/customized calibre install
|
|
||||||
=======================================================
|
|
||||||
|
|
||||||
You can "install" calibre onto a USB stick that you can take with you and use on any computer. The magic is in a .bat file called calibre-portable.bat found in the resources folder in your calibre install. Typical uses of this files are:
|
|
||||||
|
|
||||||
* Run a Mobile Calibre installation with both the Calibre binaries and your ebook library resident on a USB disk or other portable media. In particular it is not necessary to have Calibre installed on the Windows PC that is to run Calibre. This batch file also does not care what drive letter is assigned when you plug in the USB device. It also will not affect any settings on the host machine being a completely self-contained Calibre installation.
|
|
||||||
* Run a networked Calibre installation optimised for performance when the ebook files are located on a networked share.
|
|
||||||
|
|
||||||
If you find setting up the bat file too challenging, there is a third party portable calibre build available at `portableapps.com <http://portableapps.com>`_.
|
|
||||||
|
|
||||||
This calibre-portable.bat file is intended for use on Windows based systems, but the principles are easily adapted for use on Linux or OS X based systems. Note that calibre requires the Microsoft Visual C++ 2008 runtimes to run. Most windows computers have them installed already, but it may be a good idea to have the installer for installing them on your USB stick. The installer is available from `Microsoft <http://www.microsoft.com/downloads/details.aspx?FamilyID=9b2da534-3e03-4391-8a4d-074b9f2bc1bf&displaylang=en>`_.
|
|
||||||
|
|
||||||
Assumptions
|
|
||||||
------------
|
|
||||||
|
|
||||||
The calibre-portable.bat file makes the following assumptions about the folder layout that is being used::
|
|
||||||
|
|
||||||
Calibre_Root_Folder
|
|
||||||
calibre-portable.bat The batch file used to start Calibre
|
|
||||||
Calibre2 The Calibre binaries
|
|
||||||
CalibreLibrary The Calibre Library
|
|
||||||
CalibreConfig The Calibre user preferences
|
|
||||||
CalibreSource The calibre source (optional) if running a development environment.
|
|
||||||
|
|
||||||
If you want to use a different folder layout then the calibre-portable.bat file will need editing appropriately. This file can be edited using any appropriate text editor.
|
|
||||||
|
|
||||||
Preparation
|
|
||||||
------------
|
|
||||||
|
|
||||||
The steps required to prepare the USB stick are as follows:
|
|
||||||
|
|
||||||
* Decide what folder will be used to hold all the Calibre related files. If the portable media is to be dedicated to Calibre use then this can be the root folder, but if not is suggested that a folder called Calibre should be created – this will then be the Calibre_Root_Folder mentioned above and in the following steps.
|
|
||||||
* Copy the calibre-portable.bat file into the Calibre_Root_Folder.
|
|
||||||
* Create the Calibre2 folder inside the Calibre_Root_Folder to hold the Calibre binaries. There are 2 ways of populating the contents of this folder:
|
|
||||||
|
|
||||||
* The easiest is to simply copy an existing Calibre installation. Typically this would involve copying the contents of the C:\Program Files\Calibre2 folder
|
|
||||||
* Run the Calibre Windows installer:
|
|
||||||
|
|
||||||
* Tick the box to accept the GNU GPL license
|
|
||||||
* Select the Advanced option
|
|
||||||
* Change the install location to be the Calibre2 folder on the USB drive
|
|
||||||
* Deselect the options for creating Menu shortcuts; creating a calibre shortcut on the desktop; and adding Calibre to the path
|
|
||||||
|
|
||||||
* Create the CalibreLibrary folder inside the Calibre_Root_Folder. If you have an existing Calibre library copy it and all its contents to the CalibreLibrary folder. If you do not already have a library do not worry as a new one will be created at this location when Calibre is started.
|
|
||||||
* Create the CalibreConfig folder inside the Calibre_Root_Folder. This will hold your personal Calibre configuration settings. If you have an existing Calibre installation and want to copy the current settings then copy the contents of your current configuration folder to the CalibreConfig folder. You can find the location of your current configuration folder by going to :guilabel:`Preferences->Advanced->Miscellaneous` and clicking the “Open calibre configuration Directory” button.
|
|
||||||
* When you have started Calibre, go into :guilabel:`Preferences->Interface->Behavior` and check that you have set the Job Priority to ‘Low’. This setting keeps single-processor Windows systems responsive without affecting Calibre performance to any noticeable degree. On multi-processor or multi-core systems this setting does not matter as much, but setting it will do no harm.
|
|
||||||
|
|
||||||
Using calibre-portable.bat
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Once you have got the USB stick set up then the steps to use Calibre are:
|
|
||||||
|
|
||||||
* Plug the USB stick into the host machine
|
|
||||||
* Use Windows Explorer to navigate to the location of the calibre-portable.bat file on the USB stick
|
|
||||||
* Start Calibre by double-clicking the calibre-portable.bat file
|
|
||||||
* A Command Window will be opened showing the settings that are about to be used. If you are not happy with these setting use CTRL-C to abandon the batch file without starting Calibre. If you are happy then press any other key to launch Calibre with the specified settings. Once you are happy with your setup you may wish to edit the calibre-portable.bat file to eliminate this pause (add REM to the start of the line) but it a useful check that you are running with the expected settings.
|
|
||||||
|
|
||||||
Networked Installations
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
The performance of Calibre can be severely degraded if running with the Calibre library on a network share. This is primarily due to the fact that the access to the metadata.db file is slow across a network. The calibre-portable.bat file is designed to help in such scenarios. To use the calibre-portable.bat file in such a scenario the following deviations from those detailed above for the Mobile Calibre installation are needed:
|
|
||||||
|
|
||||||
* Edit the calibre-portable.bat file to specify the location of your Calibre library on the network.
|
|
||||||
* Create a CalibreMetadata folder in the Calibre_Root_Folder location. If you have an existing Calibre library then copy the metadata.db files from there to the CalibreMetadata folder.
|
|
||||||
* You can now run Calibre using the calibre-portable.bat file as specified in the previous section. One thing you should remember is to periodically copy the metadata.db file from the CalibreMetadatqa folder back to your Calibre library located on the network share.
|
|
||||||
|
|
||||||
Precautions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Portable media can occasionally fail so you should make periodic backups of you Calibre library. This can be done by making a copy of the CalibreLibrary folder and all its contents. There are many freely available tools around that can optimise such back processes, well known ones being RoboCopy and RichCopy. However you can simply use a Windows copy facility if you cannot be bothered to use a specialised tools.
|
|
||||||
|
|
||||||
Using the environment variable CALIBRE_OVERRIDE_DATABASE_PATH disables multiple-library support in |app|. Avoid setting this variable in calibre-portable.bat unless you really need it.
|
|
@ -417,7 +417,7 @@ You might find the following tips useful.
|
|||||||
|
|
||||||
* Create a custom composite column to test templates. Once you have the column, you can change its template simply by double-clicking on the column. Hide the column when you are not testing.
|
* Create a custom composite column to test templates. Once you have the column, you can change its template simply by double-clicking on the column. Hide the column when you are not testing.
|
||||||
* Templates can use other templates by referencing a composite custom column.
|
* Templates can use other templates by referencing a composite custom column.
|
||||||
* In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{null}``. This template will always evaluate to an empty string.
|
* In a plugboard, you can set a field to empty (or whatever is equivalent to empty) by using the special template ``{}``. This template will always evaluate to an empty string.
|
||||||
* The technique described above to show numbers even if they have a zero value works with the standard field series_index.
|
* The technique described above to show numbers even if they have a zero value works with the standard field series_index.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
@ -16,7 +16,6 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
|
|||||||
xpath
|
xpath
|
||||||
template_lang
|
template_lang
|
||||||
regexp
|
regexp
|
||||||
portable
|
|
||||||
server
|
server
|
||||||
creating_plugins
|
creating_plugins
|
||||||
|
|
||||||
|
@ -65,7 +65,8 @@ def test_sqlite():
|
|||||||
def test_qt():
|
def test_qt():
|
||||||
from PyQt4.Qt import (QWebView, QDialog, QImageReader, QNetworkAccessManager)
|
from PyQt4.Qt import (QWebView, QDialog, QImageReader, QNetworkAccessManager)
|
||||||
fmts = set(map(unicode, QImageReader.supportedImageFormats()))
|
fmts = set(map(unicode, QImageReader.supportedImageFormats()))
|
||||||
if 'jpg' not in fmts or 'png' not in fmts:
|
testf = set(['jpg', 'png', 'mng', 'svg', 'ico', 'gif'])
|
||||||
|
if testf.intersection(fmts) != testf:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Qt doesn't seem to be able to load its image plugins")
|
"Qt doesn't seem to be able to load its image plugins")
|
||||||
QWebView, QDialog
|
QWebView, QDialog
|
||||||
@ -87,16 +88,23 @@ def test_imaging():
|
|||||||
raise RuntimeError('PIL choked!')
|
raise RuntimeError('PIL choked!')
|
||||||
print ('PIL OK!')
|
print ('PIL OK!')
|
||||||
|
|
||||||
|
def test_unrar():
|
||||||
|
from calibre.libunrar import _libunrar
|
||||||
|
if not _libunrar:
|
||||||
|
raise RuntimeError('Failed to load libunrar')
|
||||||
|
print ('Unrar OK!')
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
test_plugins()
|
test_plugins()
|
||||||
test_lxml()
|
test_lxml()
|
||||||
test_fontconfig()
|
test_fontconfig()
|
||||||
test_sqlite()
|
test_sqlite()
|
||||||
if iswindows:
|
|
||||||
test_winutil()
|
|
||||||
test_win32()
|
|
||||||
test_qt()
|
test_qt()
|
||||||
test_imaging()
|
test_imaging()
|
||||||
|
test_unrar()
|
||||||
|
if iswindows:
|
||||||
|
test_win32()
|
||||||
|
test_winutil()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test()
|
test()
|
||||||
|
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
17024
src/calibre/translations/az.po
Normal file
17024
src/calibre/translations/az.po
Normal file
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