mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
a116f8930c
@ -109,7 +109,7 @@ function toplevel_layout() {
|
||||
var last = $(".toplevel li").last();
|
||||
var title = $('.toplevel h3').first();
|
||||
var bottom = last.position().top + last.height() - title.position().top;
|
||||
$("#main").height(Math.max(200, bottom));
|
||||
$("#main").height(Math.max(200, bottom+75));
|
||||
}
|
||||
|
||||
function toplevel() {
|
||||
@ -156,6 +156,7 @@ function category() {
|
||||
if (href) {
|
||||
$.ajax({
|
||||
url:href,
|
||||
cache: false,
|
||||
data:{'sort':cookie(sort_cookie_name)},
|
||||
success: function(data) {
|
||||
this.children(".loaded").html(data);
|
||||
@ -212,6 +213,7 @@ function load_page(elem) {
|
||||
url: href,
|
||||
context: elem,
|
||||
dataType: "json",
|
||||
cache : false,
|
||||
type: 'POST',
|
||||
timeout: 600000, //milliseconds (10 minutes)
|
||||
data: {'ids': ids},
|
||||
@ -263,6 +265,7 @@ function show_details(a_dom) {
|
||||
$.ajax({
|
||||
url: book.find('.details-href').attr('title'),
|
||||
context: bd,
|
||||
cache: false,
|
||||
dataType: "json",
|
||||
timeout: 600000, //milliseconds (10 minutes)
|
||||
error: function(xhr, stat, err) {
|
||||
|
@ -1,5 +1,9 @@
|
||||
/* CSS for the mobile version of the content server webpage */
|
||||
|
||||
.body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.navigation table.buttons {
|
||||
width: 100%;
|
||||
}
|
||||
@ -53,6 +57,7 @@ div.navigation {
|
||||
}
|
||||
#listing td {
|
||||
padding: 0.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#listing td.thumbnail {
|
||||
@ -73,6 +78,7 @@ div.navigation {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#logo {
|
||||
@ -83,4 +89,17 @@ div.navigation {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.first-line {
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.second-line {
|
||||
margin-top: 0.75ex;
|
||||
display: block;
|
||||
}
|
||||
|
@ -106,7 +106,8 @@ title_sort_articles=r'^(A|The|An)\s+'
|
||||
auto_connect_to_folder = ''
|
||||
|
||||
|
||||
# Specify renaming rules for sony collections. Collections on Sonys are named
|
||||
# Specify renaming rules for sony collections. This tweak is only applicable if
|
||||
# metadata management is set to automatic. Collections on Sonys are named
|
||||
# depending upon whether the field is standard or custom. A collection derived
|
||||
# from a standard field is named for the value in that field. For example, if
|
||||
# the standard 'series' column contains the name 'Darkover', then the series
|
||||
@ -137,6 +138,24 @@ auto_connect_to_folder = ''
|
||||
sony_collection_renaming_rules={}
|
||||
|
||||
|
||||
# Specify how sony collections are sorted. This tweak is only applicable if
|
||||
# metadata management is set to automatic. You can indicate which metadata is to
|
||||
# be used to sort on a collection-by-collection basis. The format of the tweak
|
||||
# is a list of metadata fields from which collections are made, followed by the
|
||||
# name of the metadata field containing the sort value.
|
||||
# Example: The following indicates that collections built from pubdate and tags
|
||||
# are to be sorted by the value in the custom column '#mydate', that collections
|
||||
# built from 'series' are to be sorted by 'series_index', and that all other
|
||||
# collections are to be sorted by title. If a collection metadata field is not
|
||||
# named, then if it is a series- based collection it is sorted by series order,
|
||||
# otherwise it is sorted by title order.
|
||||
# [(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], 'title')]
|
||||
# Note that the bracketing and parentheses are required. The syntax is
|
||||
# [ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]
|
||||
# Default: empty (no rules), so no collection attributes are named.
|
||||
sony_collection_sorting_rules = []
|
||||
|
||||
|
||||
# Create search terms to apply a query across several built-in search terms.
|
||||
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
|
||||
# Example: create the term 'myseries' that when used as myseries:foo would
|
||||
@ -184,3 +203,11 @@ content_server_wont_display = ['']
|
||||
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
|
||||
maximum_resort_levels = 5
|
||||
|
||||
# Absolute path to a TTF font file to use as the font for the title and author
|
||||
# when generating a default cover. Useful if the default font (Liberation
|
||||
# Serif) does not contain glyphs for the language of the books in your library.
|
||||
generate_cover_title_font = None
|
||||
|
||||
# Absolute path to a TTF font file to use as the font for the footer in the
|
||||
# default cover
|
||||
generate_cover_foot_font = None
|
||||
|
BIN
resources/images/news/theecocolapse.png
Normal file
BIN
resources/images/news/theecocolapse.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -71,7 +71,9 @@ class TheAtlantic(BasicNewsRecipe):
|
||||
for poem in soup.findAll('div', attrs={'class':'poem'}):
|
||||
title = self.tag_to_string(poem.find('h4'))
|
||||
desc = self.tag_to_string(poem.find(attrs={'class':'author'}))
|
||||
url = 'http://www.theatlantic.com'+poem.find('a')['href']
|
||||
url = poem.find('a')['href']
|
||||
if url.startswith('/'):
|
||||
url = 'http://www.theatlantic.com' + url
|
||||
self.log('\tFound article:', title, 'at', url)
|
||||
self.log('\t\t', desc)
|
||||
poems.append({'title':title, 'url':url, 'description':desc,
|
||||
@ -83,7 +85,9 @@ class TheAtlantic(BasicNewsRecipe):
|
||||
if div is not None:
|
||||
self.log('Found section: Advice')
|
||||
title = self.tag_to_string(div.find('h4'))
|
||||
url = 'http://www.theatlantic.com'+div.find('a')['href']
|
||||
url = div.find('a')['href']
|
||||
if url.startswith('/'):
|
||||
url = 'http://www.theatlantic.com' + url
|
||||
desc = self.tag_to_string(div.find('p'))
|
||||
self.log('\tFound article:', title, 'at', url)
|
||||
self.log('\t\t', desc)
|
||||
|
@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
newyorker.com
|
||||
cubadebate.cu
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
@ -13,32 +11,44 @@ class CubaDebate(BasicNewsRecipe):
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Contra el Terorismo Mediatico'
|
||||
oldest_article = 15
|
||||
language = 'es'
|
||||
|
||||
language = 'es'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
publisher = 'Cubadebate'
|
||||
category = 'news, politics, Cuba'
|
||||
encoding = 'utf-8'
|
||||
extra_css = ' #BlogTitle{font-size: x-large; font-weight: bold} '
|
||||
masthead_url = 'http://www.cubadebate.cu/wp-content/themes/cubadebate/images/logo.gif'
|
||||
publication_type = 'newsportal'
|
||||
extra_css = """
|
||||
#BlogTitle{font-size: xx-large; font-weight: bold}
|
||||
body{font-family: Verdana, Arial, Tahoma, sans-serif}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : 'es'
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
,'pretty_print': True
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'Outline'})]
|
||||
remove_tags_after = dict(name='div',attrs={'id':'BlogContent'})
|
||||
remove_tags = [dict(name='link')]
|
||||
remove_tags = [
|
||||
dict(name=['link','base','embed','object','meta','iframe'])
|
||||
,dict(attrs={'id':'addthis_container'})
|
||||
]
|
||||
|
||||
feeds = [(u'Articulos', u'http://www.cubadebate.cu/feed/')]
|
||||
remove_attributes=['width','height','lang']
|
||||
|
||||
def print_version(self, url):
|
||||
return url + 'print/'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('img'):
|
||||
if not item.has_key('alt'):
|
||||
item['alt'] = 'image'
|
||||
return soup
|
||||
|
@ -2,7 +2,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Jordi Balcells, based on an earlier version by Lorenzo Vigentini & Kovid Goyal'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
description = 'Main daily newspaper from Spain - v1.03 (03, September 2010)'
|
||||
description = 'Main daily newspaper from Spain - v1.04 (19, October 2010)'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
@ -32,19 +32,16 @@ class ElPais(BasicNewsRecipe):
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
|
||||
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','cabecera_noticia_opinion','contenido_noticia','caja_despiece','presentacion']})]
|
||||
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia_reportaje estirar','cabecera_noticia_opinion estirar','cabecera_noticia estirar','contenido_noticia','caja_despiece']})]
|
||||
|
||||
extra_css = '''
|
||||
p{style:normal size:12 serif}
|
||||
|
||||
'''
|
||||
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:200%; font-weight: bolder; text-align: justify; } h2{ font-family: sans-serif; font-size:150%; font-weight: 500; text-align: justify } h3{ font-family: sans-serif; font-size:125%; font-weight: 500; text-align: justify } img{margin-bottom: 0.4em} '
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}),
|
||||
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}),
|
||||
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos estirar','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}),
|
||||
dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas','calendar']}),
|
||||
dict(name='p', attrs={'class':'nav_meses'}),
|
||||
dict(attrs={'class':['enlaces_m','miniaturas_m']})
|
||||
dict(attrs={'class':['enlaces_m','miniaturas_m','nav_miniaturas_m']})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
|
@ -4,7 +4,6 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
foxnews.com
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class FoxNews(BasicNewsRecipe):
|
||||
@ -21,11 +20,10 @@ class FoxNews(BasicNewsRecipe):
|
||||
language = 'en'
|
||||
publication_type = 'newsportal'
|
||||
remove_empty_feeds = True
|
||||
extra_css = ' body{font-family: Arial,sans-serif } img{margin-bottom: 0.4em} .caption{font-size: x-small} '
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'</title>.*?</head>', re.DOTALL|re.IGNORECASE),lambda match: '</title></head>')
|
||||
]
|
||||
extra_css = """
|
||||
body{font-family: Arial,sans-serif }
|
||||
.caption{font-size: x-small}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -34,27 +32,15 @@ class FoxNews(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['xmlns']
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'id' :['story','browse-story-content']})
|
||||
,dict(name='div', attrs={'class':['posts articles','slideshow']})
|
||||
,dict(name='h4' , attrs={'class':'storyDate'})
|
||||
,dict(name='h1' , attrs={'xmlns:functx':'http://www.functx.com'})
|
||||
,dict(name='div', attrs={'class':'authInfo'})
|
||||
,dict(name='div', attrs={'id':'articleCont'})
|
||||
]
|
||||
remove_attributes = ['xmlns','lang']
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['share-links','quigo quigo2','share-text','storyControls','socShare','btm-links']})
|
||||
,dict(name='div', attrs={'id' :['otherMedia','loomia_display','img-all-path','story-vcmId','story-url','pane-browse-story-comments','story_related']})
|
||||
,dict(name='ul' , attrs={'class':['tools','tools alt','tools alt2','tabs']})
|
||||
,dict(name='a' , attrs={'class':'join-discussion'})
|
||||
,dict(name='ul' , attrs={'class':['tools','tools alt','tools alt2']})
|
||||
,dict(name='p' , attrs={'class':'see_fullarchive'})
|
||||
,dict(name=['object','embed','link','script'])
|
||||
dict(name=['object','embed','link','script','iframe','meta','base'])
|
||||
,dict(attrs={'class':['user-control','url-description','ad-context']})
|
||||
]
|
||||
|
||||
remove_tags_before=dict(name='h1')
|
||||
remove_tags_after =dict(attrs={'class':'url-description'})
|
||||
|
||||
feeds = [
|
||||
(u'Latest Headlines', u'http://feeds.foxnews.com/foxnews/latest' )
|
||||
@ -67,8 +53,5 @@ class FoxNews(BasicNewsRecipe):
|
||||
,(u'Entertainment' , u'http://feeds.foxnews.com/foxnews/entertainment' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return self.adeify_images(soup)
|
||||
|
||||
def print_version(self, url):
|
||||
return url + 'print'
|
||||
|
64
resources/recipes/ming_pao.recipe
Normal file
64
resources/recipes/ming_pao.recipe
Normal file
@ -0,0 +1,64 @@
|
||||
cense__ = 'GPL v3'
|
||||
__copyright__ = '2010, Eddie Lau'
|
||||
'''
|
||||
modified from Singtao Toronto calibre recipe by rty
|
||||
'''
|
||||
|
||||
import datetime
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1278063072(BasicNewsRecipe):
|
||||
title = 'Ming Pao - Hong Kong'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 100
|
||||
__author__ = 'Eddie Lau'
|
||||
description = 'Hong Kong Chinese Newspaper'
|
||||
publisher = 'news.mingpao.com'
|
||||
category = 'Chinese, News, Hong Kong'
|
||||
remove_javascript = True
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
language = 'zh'
|
||||
encoding = 'Big5-HKSCS'
|
||||
recursions = 0
|
||||
conversion_options = {'linearize_tables':True}
|
||||
masthead_url = 'http://news.mingpao.com/image/portals_top_logo_news.gif'
|
||||
|
||||
keep_only_tags = [dict(name='h1'),
|
||||
dict(attrs={'id':['newscontent01','newscontent02']})]
|
||||
|
||||
def get_fetchdate(self):
|
||||
dt_utc = datetime.datetime.utcnow()
|
||||
# convert UTC to local hk time
|
||||
dt_local = dt_utc - datetime.timedelta(-8.0/24)
|
||||
return dt_local.strftime("%Y%m%d")
|
||||
|
||||
def parse_index(self):
|
||||
feeds = []
|
||||
dateStr = self.get_fetchdate()
|
||||
for title, url in [(u'\u8981\u805e Headline', 'http://news.mingpao.com/' + dateStr + '/gaindex.htm'), (u'\u6559\u80b2 Education', 'http://news.mingpao.com/' + dateStr + '/gfindex.htm'), (u'\u6e2f\u805e Local', 'http://news.mingpao.com/' + dateStr + '/gbindex.htm'), (u'\u793e\u8a55\u2027\u7b46\u9663 Editorial', 'http://news.mingpao.com/' + dateStr + '/mrindex.htm'), (u'\u8ad6\u58c7 Forum', 'http://news.mingpao.com/' + dateStr + '/faindex.htm'), (u'\u4e2d\u570b China', 'http://news.mingpao.com/' + dateStr + '/caindex.htm'), (u'\u570b\u969b World', 'http://news.mingpao.com/' + dateStr + '/taindex.htm'), ('Tech News', 'http://news.mingpao.com/' + dateStr + '/naindex.htm'), (u'\u9ad4\u80b2 Sport', 'http://news.mingpao.com/' + dateStr + '/spindex.htm'), (u'\u526f\u520a Supplement', 'http://news.mingpao.com/' + dateStr + '/jaindex.htm'),]:
|
||||
articles = self.parse_section(url)
|
||||
if articles:
|
||||
feeds.append((title, articles))
|
||||
return feeds
|
||||
|
||||
def parse_section(self, url):
|
||||
dateStr = self.get_fetchdate()
|
||||
soup = self.index_to_soup(url)
|
||||
divs = soup.findAll(attrs={'class': ['bullet']})
|
||||
current_articles = []
|
||||
for i in divs:
|
||||
a = i.find('a', href = True)
|
||||
title = self.tag_to_string(a)
|
||||
url = a.get('href', False)
|
||||
url = 'http://news.mingpao.com/' + dateStr + '/' +url
|
||||
current_articles.append({'title': title, 'url': url, 'description':''})
|
||||
return current_articles
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll(width=True):
|
||||
del item['width']
|
||||
return soup
|
||||
|
@ -8,11 +8,11 @@ import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class NewScientist(BasicNewsRecipe):
|
||||
title = 'New Scientist - Online News'
|
||||
title = 'New Scientist - Online News w. subscription'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Science news and science articles from New Scientist.'
|
||||
language = 'en'
|
||||
publisher = 'New Scientist'
|
||||
publisher = 'Reed Business Information Ltd.'
|
||||
category = 'science news, science articles, science jobs, drugs, cancer, depression, computer software'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
@ -21,7 +21,12 @@ class NewScientist(BasicNewsRecipe):
|
||||
cover_url = 'http://www.newscientist.com/currentcover.jpg'
|
||||
masthead_url = 'http://www.newscientist.com/img/misc/ns_logo.jpg'
|
||||
encoding = 'utf-8'
|
||||
extra_css = ' body{font-family: Arial,sans-serif} img{margin-bottom: 0.8em} '
|
||||
needs_subscription = 'optional'
|
||||
extra_css = """
|
||||
body{font-family: Arial,sans-serif}
|
||||
img{margin-bottom: 0.8em}
|
||||
.quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -33,15 +38,27 @@ class NewScientist(BasicNewsRecipe):
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':['pgtop','maincol','blgmaincol','nsblgposts','hldgalcols']})]
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
br.open('http://www.newscientist.com/')
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('https://www.newscientist.com/user/login?redirectURL=')
|
||||
br.select_form(nr=2)
|
||||
br['loginId' ] = self.username
|
||||
br['password'] = self.password
|
||||
br.submit()
|
||||
return br
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]})
|
||||
,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial','sharebtns']})
|
||||
,dict(name='p' , attrs={'class':['marker','infotext' ]})
|
||||
,dict(name='meta' , attrs={'name' :'description' })
|
||||
,dict(name='a' , attrs={'rel' :'tag' })
|
||||
,dict(name='a' , attrs={'rel' :'tag' })
|
||||
,dict(name=['link','base','meta','iframe','object','embed'])
|
||||
]
|
||||
remove_tags_after = dict(attrs={'class':['nbpcopy','comments']})
|
||||
remove_attributes = ['height','width']
|
||||
remove_attributes = ['height','width','lang']
|
||||
|
||||
feeds = [
|
||||
(u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
|
||||
@ -62,6 +79,8 @@ class NewScientist(BasicNewsRecipe):
|
||||
return url + '?full=true&print=true'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(['quote','quotetext']):
|
||||
item.name='p'
|
||||
for tg in soup.findAll('a'):
|
||||
if tg.string == 'Home':
|
||||
tg.parent.extract()
|
||||
|
@ -4,149 +4,79 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''
|
||||
nytimes.com
|
||||
V5 - One picture per article, moved to top:
|
||||
Headline
|
||||
Image
|
||||
Byline
|
||||
Story
|
||||
'''
|
||||
import re, string, time
|
||||
import string, re, time
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, Tag
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
|
||||
def decode(self, src):
|
||||
enc = 'utf-8'
|
||||
if 'iso-8859-1' in src:
|
||||
enc = 'cp1252'
|
||||
return src.decode(enc, 'ignore')
|
||||
|
||||
class NYTimes(BasicNewsRecipe):
|
||||
|
||||
title = 'The New York Times'
|
||||
__author__ = 'GRiker'
|
||||
title = u'New York Times'
|
||||
__author__ = 'Kovid Goyal/Nick Redding'
|
||||
language = 'en'
|
||||
requires_version = (0, 7, 5)
|
||||
requires_version = (0, 6, 36)
|
||||
|
||||
description = 'Daily news from the New York Times (subscription version)'
|
||||
allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials',
|
||||
'New York','Business Day','Science Times','Sports','Dining','Arts',
|
||||
'Home','Styles','Sunday Business','Week In Review','Travel','Magazine',
|
||||
'Book Review','Weddings','Real Estate','Automobiles',"T Men's Fashion",
|
||||
"T Women's Fashion"]
|
||||
|
||||
# List of sections to exclude
|
||||
# To add a section, copy the section name from the allSectionKeywords list above
|
||||
# For example, to exclude 'Dining' and 'Weddings':
|
||||
#excludeSectionKeywords = ['Dining','Weddings']
|
||||
excludeSectionKeywords = []
|
||||
|
||||
# List of sections to include (test and debug only)
|
||||
# By default, any sections in today's paper that are not listed in excludeSectionKeywords
|
||||
# are downloaded. fetch_only specifies that only certain sections are to be downloaded.
|
||||
# This should only be used for testing and debugging.
|
||||
# For example, to download only 'The Front Page' section:
|
||||
# fetch_only = set(['The Front Page'])
|
||||
fetch_only = set([])
|
||||
if fetch_only:
|
||||
excludeSectionKeywords = list(set(allSectionKeywords) ^ fetch_only)
|
||||
|
||||
# one_picture_per_article specifies that calibre should only use the first image
|
||||
# from an article (if one exists). If one_picture_per_article = True, the image
|
||||
# will be moved to a location between the headline and the byline.
|
||||
# If one_picture_per_article = False, all images from the article will be included
|
||||
# and shown in their original location.
|
||||
one_picture_per_article = True
|
||||
|
||||
timefmt = ''
|
||||
timefmt = ' [%b %d]'
|
||||
needs_subscription = True
|
||||
remove_tags_before = dict(id='article')
|
||||
remove_tags_after = dict(id='article')
|
||||
remove_tags = [dict(attrs={'class':[
|
||||
'articleFooter',
|
||||
'articleTools',
|
||||
'columnGroup doubleRule',
|
||||
'columnGroup singleRule',
|
||||
'columnGroup last',
|
||||
'columnGroup last',
|
||||
'doubleRule',
|
||||
'dottedLine',
|
||||
'entry-meta',
|
||||
'entry-response module',
|
||||
'icon enlargeThis',
|
||||
'leftNavTabs',
|
||||
'module box nav',
|
||||
'nextArticleLink',
|
||||
'nextArticleLink clearfix',
|
||||
'post-tools',
|
||||
'relatedSearchesModule',
|
||||
'side_tool',
|
||||
'singleAd',
|
||||
'subNavigation clearfix',
|
||||
'subNavigation tabContent active',
|
||||
'subNavigation tabContent active clearfix',
|
||||
]}),
|
||||
dict(id=[
|
||||
'adxLeaderboard',
|
||||
'archive',
|
||||
'articleExtras',
|
||||
'articleInline',
|
||||
'blog_sidebar',
|
||||
'businessSearchBar',
|
||||
'cCol',
|
||||
'entertainmentSearchBar',
|
||||
'footer',
|
||||
'header',
|
||||
'header_search',
|
||||
'login',
|
||||
'masthead',
|
||||
'masthead-nav',
|
||||
'memberTools',
|
||||
'navigation',
|
||||
'portfolioInline',
|
||||
'relatedArticles',
|
||||
'respond',
|
||||
'side_search',
|
||||
'side_index',
|
||||
'side_tool',
|
||||
'toolsRight',
|
||||
]),
|
||||
dict(name=['script', 'noscript', 'style'])]
|
||||
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
||||
cover_margins = (18,18,'grey99')
|
||||
remove_tags = [dict(attrs={'class':['articleTools', 'post-tools', 'side_tool','nextArticleLink',
|
||||
'nextArticleLink clearfix','columnGroup doubleRule','doubleRule','entry-meta',
|
||||
'icon enlargeThis','columnGroup last','relatedSearchesModule']}),
|
||||
dict({'class':re.compile('^subNavigation')}),
|
||||
dict({'class':re.compile('^leaderboard')}),
|
||||
dict({'class':re.compile('^module')}),
|
||||
dict({'class':'metaFootnote'}),
|
||||
dict(id=['inlineBox','footer', 'toolsRight', 'articleInline','login','masthead',
|
||||
'navigation', 'archive', 'side_search', 'blog_sidebar','cCol','portfolioInline',
|
||||
'side_tool', 'side_index','header','readerReviewsCount','readerReviews',
|
||||
'relatedArticles', 'relatedTopics', 'adxSponLink']),
|
||||
dict(name=['script', 'noscript', 'style','form','hr'])]
|
||||
encoding = decode
|
||||
no_stylesheets = True
|
||||
extra_css = '.headline {text-align: left;}\n \
|
||||
.byline {font-family: monospace; \
|
||||
text-align: left; \
|
||||
margin-top: 0px; \
|
||||
margin-bottom: 0px;}\n \
|
||||
.dateline {font-size: small; \
|
||||
margin-top: 0px; \
|
||||
margin-bottom: 0px;}\n \
|
||||
.timestamp {font-size: small; \
|
||||
margin-top: 0px; \
|
||||
margin-bottom: 0px;}\n \
|
||||
.source {text-align: left;}\n \
|
||||
.image {text-align: center;}\n \
|
||||
.credit {text-align: right; \
|
||||
font-size: small; \
|
||||
margin-top: 0px; \
|
||||
margin-bottom: 0px;}\n \
|
||||
.articleBody {text-align: left;}\n \
|
||||
.authorId {text-align: left; \
|
||||
font-style: italic;}\n '
|
||||
extra_css = '''
|
||||
.articleHeadline { margin-top:0.5em; margin-bottom:0.25em; }
|
||||
.credit { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
.byline { font-size: small; font-style:italic; line-height:1em; margin-top:10px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
.dateline { font-size: small; line-height:1em;margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
.kicker { font-size: small; line-height:1em;margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
.timestamp { font-size: small; }
|
||||
.caption { font-size: small; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
a:link {text-decoration: none; }'''
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
try:
|
||||
br.open('http://www.nytimes.com/auth/login')
|
||||
br.select_form(name='login')
|
||||
br['USERID'] = self.username
|
||||
br['PASSWORD'] = self.password
|
||||
raw = br.submit().read()
|
||||
if 'Sorry, we could not find the combination you entered. Please try again.' in raw:
|
||||
raise Exception('Your username and password are incorrect')
|
||||
#open('/t/log.html', 'wb').write(raw)
|
||||
except:
|
||||
self.log("\nFailed to login")
|
||||
|
||||
br.open('http://www.nytimes.com/auth/login')
|
||||
br.select_form(name='login')
|
||||
br['USERID'] = self.username
|
||||
br['PASSWORD'] = self.password
|
||||
raw = br.submit().read()
|
||||
if 'Sorry, we could not find the combination you entered. Please try again.' in raw:
|
||||
raise Exception('Your username and password are incorrect')
|
||||
#open('/t/log.html', 'wb').write(raw)
|
||||
return br
|
||||
|
||||
def get_masthead_url(self):
|
||||
masthead = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
||||
#masthead = 'http://members.cox.net/nickredding/nytlogo.gif'
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
try:
|
||||
br.open(masthead)
|
||||
except:
|
||||
self.log("\nMasthead unavailable")
|
||||
masthead = None
|
||||
return masthead
|
||||
|
||||
|
||||
def get_cover_url(self):
|
||||
cover = None
|
||||
st = time.localtime()
|
||||
@ -162,316 +92,101 @@ class NYTimes(BasicNewsRecipe):
|
||||
cover = None
|
||||
return cover
|
||||
|
||||
def get_masthead_title(self):
|
||||
return self.title
|
||||
|
||||
def dump_ans(self, ans):
|
||||
total_article_count = 0
|
||||
for section in ans :
|
||||
if self.verbose:
|
||||
self.log("section %s: %d articles" % (section[0], len(section[1])) )
|
||||
for article in section[1]:
|
||||
total_article_count += 1
|
||||
if self.verbose:
|
||||
self.log("\t%-40.40s... \t%-60.60s..." % (article['title'].encode('mac-roman','replace'),
|
||||
article['url'].encode('mac-roman','replace')))
|
||||
self.log( "Queued %d articles" % total_article_count )
|
||||
|
||||
def dump_hex(self, src, length=16):
|
||||
''' Diagnostic '''
|
||||
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
|
||||
N=0; result=''
|
||||
while src:
|
||||
s,src = src[:length],src[length:]
|
||||
hexa = ' '.join(["%02X"%ord(x) for x in s])
|
||||
s = s.translate(FILTER)
|
||||
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
|
||||
N+=length
|
||||
print result
|
||||
|
||||
def fixChars(self,string):
|
||||
# Replace lsquo (\x91)
|
||||
fixed = re.sub("\x91","‘",string)
|
||||
|
||||
# Replace rsquo (\x92)
|
||||
fixed = re.sub("\x92","’",fixed)
|
||||
|
||||
# Replace ldquo (\x93)
|
||||
fixed = re.sub("\x93","“",fixed)
|
||||
|
||||
# Replace rdquo (\x94)
|
||||
fixed = re.sub("\x94","”",fixed)
|
||||
|
||||
# Replace ndash (\x96)
|
||||
fixed = re.sub("\x96","–",fixed)
|
||||
|
||||
# Replace mdash (\x97)
|
||||
fixed = re.sub("\x97","—",fixed)
|
||||
|
||||
return fixed
|
||||
|
||||
def massageNCXText(self, description):
|
||||
# Kindle TOC descriptions won't render certain characters
|
||||
if description:
|
||||
massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
|
||||
# Replace '&' with '&'
|
||||
massaged = re.sub("&","&", massaged)
|
||||
return self.fixChars(massaged)
|
||||
else:
|
||||
return description
|
||||
def short_title(self):
|
||||
return 'New York Times'
|
||||
|
||||
def parse_index(self):
|
||||
self.encoding = 'cp1252'
|
||||
soup = self.index_to_soup('http://www.nytimes.com/pages/todayspaper/index.html')
|
||||
self.encoding = decode
|
||||
|
||||
def feed_title(div):
|
||||
return ''.join(div.findAll(text=True, recursive=False)).strip()
|
||||
return ''.join(div.findAll(text=True, recursive=True)).strip()
|
||||
|
||||
articles = {}
|
||||
key = None
|
||||
ans = []
|
||||
# Find each instance of class="section-headline", class="story", class="story headline"
|
||||
for div in soup.findAll(True,
|
||||
attrs={'class':['section-headline', 'story', 'story headline']}):
|
||||
url_list = []
|
||||
|
||||
if div['class'] == 'section-headline':
|
||||
key = string.capwords(feed_title(div))
|
||||
if self.excludeSectionKeywords:
|
||||
excluded = re.compile('|'.join(self.excludeSectionKeywords))
|
||||
if excluded.search(key):
|
||||
self.log("Skipping section %s" % key)
|
||||
continue
|
||||
articles[key] = []
|
||||
ans.append(key)
|
||||
|
||||
elif div['class'] in ['story', 'story headline'] :
|
||||
a = div.find('a', href=True)
|
||||
if not a:
|
||||
continue
|
||||
url = re.sub(r'\?.*', '', a['href'])
|
||||
url += '?pagewanted=all'
|
||||
|
||||
title = self.massageNCXText(self.tag_to_string(a, use_alt=True).strip())
|
||||
|
||||
description = ''
|
||||
pubdate = strftime('%a, %d %b')
|
||||
summary = div.find(True, attrs={'class':'summary'})
|
||||
if summary:
|
||||
description = self.massageNCXText(self.tag_to_string(summary, use_alt=False))
|
||||
|
||||
author = ''
|
||||
authorAttribution = div.find(True, attrs={'class':'storyheadline-author'})
|
||||
def handle_article(div):
|
||||
a = div.find('a', href=True)
|
||||
if not a:
|
||||
return
|
||||
url = re.sub(r'\?.*', '', a['href'])
|
||||
if not url.startswith("http"):
|
||||
return
|
||||
if not url.endswith(".html"):
|
||||
return
|
||||
if 'podcast' in url:
|
||||
return
|
||||
url += '?pagewanted=all'
|
||||
if url in url_list:
|
||||
return
|
||||
url_list.append(url)
|
||||
title = self.tag_to_string(a, use_alt=True).strip()
|
||||
#self.log("Title: %s" % title)
|
||||
description = ''
|
||||
pubdate = strftime('%a, %d %b')
|
||||
summary = div.find(True, attrs={'class':'summary'})
|
||||
if summary:
|
||||
description = self.tag_to_string(summary, use_alt=False)
|
||||
author = ''
|
||||
authorAttribution = div.find(True, attrs={'class':'byline'})
|
||||
if authorAttribution:
|
||||
author = self.tag_to_string(authorAttribution, use_alt=False)
|
||||
else:
|
||||
authorAttribution = div.find(True, attrs={'class':'byline'})
|
||||
if authorAttribution:
|
||||
author = self.tag_to_string(authorAttribution, use_alt=False)
|
||||
else:
|
||||
authorAttribution = div.find(True, attrs={'class':'byline'})
|
||||
if authorAttribution:
|
||||
author = self.tag_to_string(authorAttribution, use_alt=False)
|
||||
# Kill commas - Kindle switches to '&'
|
||||
author = re.sub(',','',author)
|
||||
feed = key if key is not None else 'Uncategorized'
|
||||
if not articles.has_key(feed):
|
||||
articles[feed] = []
|
||||
articles[feed].append(
|
||||
dict(title=title, url=url, date=pubdate,
|
||||
description=description, author=author,
|
||||
content=''))
|
||||
|
||||
feed = key if key is not None else 'Uncategorized'
|
||||
if not articles.has_key(feed):
|
||||
articles[feed] = []
|
||||
if not 'podcasts' in url:
|
||||
articles[feed].append(
|
||||
dict(title=title, url=url, date=pubdate,
|
||||
description=description, author=author,
|
||||
content=''))
|
||||
ans = self.sort_index_by(ans, {'The Front Page':-1,
|
||||
'Dining In, Dining Out':1,
|
||||
'Obituaries':2})
|
||||
|
||||
|
||||
# Find each instance of class="section-headline", class="story", class="story headline"
|
||||
for div in soup.findAll(True,
|
||||
attrs={'class':['section-headline', 'story', 'story headline','sectionHeader','headlinesOnly multiline flush']}):
|
||||
|
||||
if div['class'] in ['section-headline','sectionHeader']:
|
||||
key = string.capwords(feed_title(div))
|
||||
articles[key] = []
|
||||
ans.append(key)
|
||||
#self.log('Section: %s' % key)
|
||||
|
||||
elif div['class'] in ['story', 'story headline'] :
|
||||
handle_article(div)
|
||||
elif div['class'] == 'headlinesOnly multiline flush':
|
||||
for lidiv in div.findAll('li'):
|
||||
handle_article(lidiv)
|
||||
|
||||
# ans = self.sort_index_by(ans, {'The Front Page':-1,
|
||||
# 'Dining In, Dining Out':1,
|
||||
# 'Obituaries':2})
|
||||
ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
|
||||
self.dump_ans(ans)
|
||||
|
||||
return ans
|
||||
|
||||
def skip_ad_pages(self, soup):
|
||||
# Skip ad pages served before actual article
|
||||
skip_tag = soup.find(True, {'name':'skip'})
|
||||
if skip_tag is not None:
|
||||
self.log.warn("Found forwarding link: %s" % skip_tag.parent['href'])
|
||||
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
|
||||
url += '?pagewanted=all'
|
||||
self.log.warn("Skipping ad to article at '%s'" % url)
|
||||
return self.index_to_soup(url, raw=True)
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.strip_anchors(soup)
|
||||
kicker_tag = soup.find(attrs={'class':'kicker'})
|
||||
if kicker_tag:
|
||||
tagline = self.tag_to_string(kicker_tag)
|
||||
#self.log("FOUND KICKER %s" % tagline)
|
||||
if tagline=='Op-Ed Columnist':
|
||||
img_div = soup.find('div','inlineImage module')
|
||||
#self.log("Searching for photo")
|
||||
if img_div:
|
||||
img_div.extract()
|
||||
#self.log("Photo deleted")
|
||||
refresh = soup.find('meta', {'http-equiv':'refresh'})
|
||||
if refresh is None:
|
||||
return soup
|
||||
content = refresh.get('content').partition('=')[2]
|
||||
raw = self.browser.open_novisit('http://www.nytimes.com'+content).read()
|
||||
return BeautifulSoup(raw.decode('cp1252', 'replace'))
|
||||
|
||||
def postprocess_html(self,soup, True):
|
||||
print "\npostprocess_html()\n"
|
||||
|
||||
if self.one_picture_per_article:
|
||||
# Remove all images after first
|
||||
largeImg = soup.find(True, {'class':'articleSpanImage'})
|
||||
inlineImgs = soup.findAll(True, {'class':'inlineImage module'})
|
||||
if largeImg:
|
||||
for inlineImg in inlineImgs:
|
||||
inlineImg.extract()
|
||||
else:
|
||||
if inlineImgs:
|
||||
firstImg = inlineImgs[0]
|
||||
for inlineImg in inlineImgs[1:]:
|
||||
inlineImg.extract()
|
||||
# Move firstImg after headline
|
||||
cgFirst = soup.find(True, {'class':'columnGroup first'})
|
||||
if cgFirst:
|
||||
# Strip all sibling NavigableStrings: noise
|
||||
navstrings = cgFirst.findAll(text=True, recursive=False)
|
||||
[ns.extract() for ns in navstrings]
|
||||
headline_found = False
|
||||
tag = cgFirst.find(True)
|
||||
insertLoc = 0
|
||||
while True:
|
||||
insertLoc += 1
|
||||
if hasattr(tag,'class') and tag['class'] == 'articleHeadline':
|
||||
headline_found = True
|
||||
break
|
||||
tag = tag.nextSibling
|
||||
if not tag:
|
||||
headline_found = False
|
||||
break
|
||||
if headline_found:
|
||||
cgFirst.insert(insertLoc,firstImg)
|
||||
else:
|
||||
self.log(">>> No class:'columnGroup first' found <<<")
|
||||
# Change class="kicker" to <h3>
|
||||
kicker = soup.find(True, {'class':'kicker'})
|
||||
if kicker and kicker.contents and kicker.contents[0]:
|
||||
h3Tag = Tag(soup, "h3")
|
||||
h3Tag.insert(0, self.fixChars(self.tag_to_string(kicker,
|
||||
use_alt=False)))
|
||||
kicker.replaceWith(h3Tag)
|
||||
|
||||
# Change captions to italic -1
|
||||
for caption in soup.findAll(True, {'class':'caption'}) :
|
||||
if caption and caption.contents[0]:
|
||||
emTag = Tag(soup, "em")
|
||||
c = self.fixChars(self.tag_to_string(caption,use_alt=False)).strip()
|
||||
mp_off = c.find("More Photos")
|
||||
if mp_off >= 0:
|
||||
c = c[:mp_off]
|
||||
emTag.insert(0, c)
|
||||
#hrTag = Tag(soup, 'hr')
|
||||
#hrTag['class'] = 'caption_divider'
|
||||
hrTag = Tag(soup, 'div')
|
||||
hrTag['class'] = 'divider'
|
||||
emTag.insert(1, hrTag)
|
||||
caption.replaceWith(emTag)
|
||||
|
||||
# Change <nyt_headline> to <h2>
|
||||
h1 = soup.find('h1')
|
||||
if h1:
|
||||
headline = h1.find("nyt_headline")
|
||||
if headline:
|
||||
tag = Tag(soup, "h2")
|
||||
tag['class'] = "headline"
|
||||
tag.insert(0, self.fixChars(headline.contents[0]))
|
||||
h1.replaceWith(tag)
|
||||
else:
|
||||
# Blog entry - replace headline, remove <hr> tags
|
||||
headline = soup.find('title')
|
||||
if headline:
|
||||
tag = Tag(soup, "h2")
|
||||
tag['class'] = "headline"
|
||||
tag.insert(0, self.fixChars(headline.contents[0]))
|
||||
soup.insert(0, tag)
|
||||
hrs = soup.findAll('hr')
|
||||
for hr in hrs:
|
||||
hr.extract()
|
||||
|
||||
# Change <h1> to <h3> - used in editorial blogs
|
||||
masthead = soup.find("h1")
|
||||
if masthead:
|
||||
# Nuke the href
|
||||
if masthead.a:
|
||||
del(masthead.a['href'])
|
||||
tag = Tag(soup, "h3")
|
||||
tag.insert(0, self.fixChars(masthead.contents[0]))
|
||||
masthead.replaceWith(tag)
|
||||
|
||||
# Change <span class="bold"> to <b>
|
||||
for subhead in soup.findAll(True, {'class':'bold'}) :
|
||||
if subhead.contents:
|
||||
bTag = Tag(soup, "b")
|
||||
bTag.insert(0, subhead.contents[0])
|
||||
subhead.replaceWith(bTag)
|
||||
|
||||
# Synthesize a section header
|
||||
dsk = soup.find('meta', attrs={'name':'dsk'})
|
||||
if dsk and dsk.has_key('content'):
|
||||
hTag = Tag(soup,'h3')
|
||||
hTag['class'] = 'section'
|
||||
hTag.insert(0,NavigableString(dsk['content']))
|
||||
articleTag = soup.find(True, attrs={'id':'article'})
|
||||
if articleTag:
|
||||
articleTag.insert(0,hTag)
|
||||
|
||||
# Add class="articleBody" to <div> so we can format with CSS
|
||||
divTag = soup.find('div',attrs={'id':'articleBody'})
|
||||
if divTag:
|
||||
divTag['class'] = divTag['id']
|
||||
|
||||
# Add class="authorId" to <div> so we can format with CSS
|
||||
divTag = soup.find('div',attrs={'id':'authorId'})
|
||||
if divTag and divTag.contents[0]:
|
||||
tag = Tag(soup, "p")
|
||||
tag['class'] = "authorId"
|
||||
tag.insert(0, self.fixChars(self.tag_to_string(divTag.contents[0],
|
||||
use_alt=False)))
|
||||
divTag.replaceWith(tag)
|
||||
|
||||
return soup
|
||||
|
||||
def populate_article_metadata(self,article,soup,first):
|
||||
'''
|
||||
Extract author and description from article, add to article metadata
|
||||
'''
|
||||
def extract_author(soup):
|
||||
byline = soup.find('meta',attrs={'name':['byl','CLMST']})
|
||||
if byline :
|
||||
author = byline['content']
|
||||
else :
|
||||
# Try for <div class="byline">
|
||||
byline = soup.find('div', attrs={'class':'byline'})
|
||||
if byline:
|
||||
author = byline.renderContents()
|
||||
else:
|
||||
print soup.prettify()
|
||||
return None
|
||||
return author
|
||||
|
||||
def extract_description(soup):
|
||||
description = soup.find('meta',attrs={'name':['description','description ']})
|
||||
if description :
|
||||
return self.massageNCXText(description['content'])
|
||||
else:
|
||||
# Take first paragraph of article
|
||||
articlebody = soup.find('div',attrs={'id':'articlebody'})
|
||||
if not articlebody:
|
||||
# Try again with class instead of id
|
||||
articlebody = soup.find('div',attrs={'class':'articlebody'})
|
||||
if not articlebody:
|
||||
print 'postprocess_book.extract_description(): Did not find <div id="articlebody">:'
|
||||
print soup.prettify()
|
||||
return None
|
||||
paras = articlebody.findAll('p')
|
||||
for p in paras:
|
||||
if p.renderContents() > '' :
|
||||
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
|
||||
return None
|
||||
|
||||
if not article.author:
|
||||
article.author = extract_author(soup)
|
||||
if not article.summary:
|
||||
article.summary = article.text_summary = extract_description(soup)
|
||||
|
||||
def strip_anchors(self,soup):
|
||||
paras = soup.findAll(True)
|
||||
for para in paras:
|
||||
aTags = para.findAll('a')
|
||||
for a in aTags:
|
||||
if a.img is None:
|
||||
a.replaceWith(a.renderContents().decode('utf-8','replace'))
|
||||
#a.replaceWith(a.renderContents().decode('cp1252','replace'))
|
||||
return soup
|
||||
|
||||
|
60
resources/recipes/stnn.recipe
Normal file
60
resources/recipes/stnn.recipe
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Larry Chan <larry1chan at gmail.com>'
|
||||
'''
|
||||
Singtao STNN
|
||||
'''
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class SingtaoSTNN(BasicNewsRecipe):
|
||||
title = 'Singtao STNN'
|
||||
__author__ = 'Larry Chan, larry1chan'
|
||||
description = 'Chinese News'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 100
|
||||
simultaneous_downloads = 5
|
||||
no_stylesheets = True
|
||||
#delay = 1
|
||||
use_embedded_content = False
|
||||
encoding = 'gb2312'
|
||||
publisher = 'Singtao STNN'
|
||||
category = 'news, China, world'
|
||||
language = 'zh'
|
||||
publication_type = 'newsportal'
|
||||
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
|
||||
masthead_url = 'http://www.stnn.cc/images/0806/logo_080728.gif'
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
,'linearize_tables': True
|
||||
}
|
||||
|
||||
|
||||
remove_tags_before = dict(name='div', attrs={'class':['page_box']})
|
||||
remove_tags_after = dict(name='div', attrs={'class':['pagelist']})
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['font_title clearfix']}),
|
||||
dict(name='div', attrs={'id':['content_zoom']})
|
||||
|
||||
]
|
||||
|
||||
remove_attributes = ['width','height','href']
|
||||
|
||||
# for a full list of rss check out [url]http://www.stnn.cc/rss/[/url]
|
||||
|
||||
feeds = [ (u'Headline News', u'http://www.stnn.cc/rss/news/index.xml'),
|
||||
(u'Breaking News', u'http://www.stnn.cc/rss/tufa/index.xml'),
|
||||
(u'Finance', u'http://www.stnn.cc/rss/fin/index.xml'),
|
||||
(u'Entertainment', u'http://www.stnn.cc/rss/ent/index.xml'),
|
||||
(u'International', u'http://www.stnn.cc/rss/guoji/index.xml'),
|
||||
(u'China', u'http://www.stnn.cc/rss/china/index.xml'),
|
||||
(u'Opnion', u'http://www.stnn.cc/rss/fin_op/index.xml'),
|
||||
(u'Blog', u'http://blog.stnn.cc/uploadfile/rssblogtypehotlog.xml'),
|
||||
(u'Hong Kong', u'http://www.stnn.cc/rss/hongkong/index.xml')
|
||||
|
||||
]
|
||||
|
46
resources/recipes/theecocolapse.recipe
Normal file
46
resources/recipes/theecocolapse.recipe
Normal file
@ -0,0 +1,46 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
theeconomiccollapseblog.com
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class TheEconomicCollapse(BasicNewsRecipe):
|
||||
title = 'The Economic Collapse'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Are You Prepared For The Coming Economic Collapse And The Next Great Depression?'
|
||||
publisher = 'The Economic Collapse'
|
||||
category = 'news, politics, USA, economy'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
remove_empty_feeds = True
|
||||
extra_css = """
|
||||
body{font-family: Tahoma,Arial,sans-serif }
|
||||
img{margin-bottom: 0.4em}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(attrs={'class':'sociable'})
|
||||
,dict(name=['iframe','object','embed','meta','link','base'])
|
||||
]
|
||||
remove_attributes=['lang','onclick','width','height']
|
||||
keep_only_tags=[dict(attrs={'class':['post-headline','post-bodycopy clearfix','']})]
|
||||
|
||||
feeds = [(u'Posts', u'http://theeconomiccollapseblog.com/feed')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return self.adeify_images(soup)
|
||||
|
@ -21,8 +21,9 @@ class TheEconomicTimes(BasicNewsRecipe):
|
||||
language = 'en_IN'
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://economictimes.indiatimes.com/photo/2676871.cms'
|
||||
extra_css = """ body{font-family: Arial,Helvetica,sans-serif}
|
||||
.heading1{font-size: xx-large; font-weight: bold} """
|
||||
extra_css = """
|
||||
body{font-family: Arial,Helvetica,sans-serif}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -31,8 +32,9 @@ class TheEconomicTimes(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(attrs={'class':['heading1','headingnext','Normal']})]
|
||||
keep_only_tags = [dict(attrs={'class':'printdiv'})]
|
||||
remove_tags = [dict(name=['object','link','embed','iframe','base','table','meta'])]
|
||||
remove_attributes = ['name']
|
||||
|
||||
feeds = [(u'All articles', u'http://economictimes.indiatimes.com/rssfeedsdefault.cms')]
|
||||
|
||||
|
@ -54,7 +54,7 @@ reflow_error = poppler_error if poppler_error else magick_error
|
||||
|
||||
pdfreflow_libs = []
|
||||
if iswindows:
|
||||
pdfreflow_libs = ['advapi32', 'User32', 'Gdi32']
|
||||
pdfreflow_libs = ['advapi32', 'User32', 'Gdi32', 'zlib']
|
||||
|
||||
extensions = [
|
||||
|
||||
|
@ -213,7 +213,7 @@ It contains correct fonts.conf etc.
|
||||
poppler
|
||||
-------------
|
||||
|
||||
In Cmake: disable GTK, Qt, OPenjpeg, zlib, lcms, gtk_tests, qt_tests. Enable qt4, jpeg, png and zlib
|
||||
In Cmake: disable GTK, Qt, OPenjpeg, cpp, lcms, gtk_tests, qt_tests. Enable qt4, jpeg, png and zlib
|
||||
|
||||
NOTE: poppler must be built as a static library, unless you build the qt4 bindings
|
||||
|
||||
|
@ -294,3 +294,8 @@ class OutputFormatPlugin(Plugin):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_periodical(self):
|
||||
return self.oeb.metadata.publication_type and \
|
||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:')
|
||||
|
||||
|
@ -4,6 +4,7 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
from itertools import izip
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
@ -417,6 +418,13 @@ class iPadOutput(OutputProfile):
|
||||
'''
|
||||
# }}}
|
||||
|
||||
class TabletOutput(iPadOutput):
|
||||
name = 'Tablet'
|
||||
short_name = 'tablet'
|
||||
description = _('Intended for generic tablet devices, does no resizing of images')
|
||||
|
||||
screen_size = (sys.maxint, sys.maxint)
|
||||
comic_screen_size = (sys.maxint, sys.maxint)
|
||||
|
||||
class SonyReaderOutput(OutputProfile):
|
||||
|
||||
@ -583,7 +591,8 @@ class KindleDXOutput(OutputProfile):
|
||||
# Screen size is a best guess
|
||||
screen_size = (744, 1022)
|
||||
dpi = 150.0
|
||||
comic_screen_size = (741, 1022)
|
||||
comic_screen_size = (771, 1116)
|
||||
#comic_screen_size = (741, 1022)
|
||||
supports_mobi_indexing = True
|
||||
periodical_date_in_title = False
|
||||
mobi_ems_per_blockquote = 2.0
|
||||
@ -663,7 +672,7 @@ class BambookOutput(OutputProfile):
|
||||
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
||||
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
||||
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
|
||||
iPadOutput, KoboReaderOutput,
|
||||
iPadOutput, KoboReaderOutput, TabletOutput,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||
BambookOutput, ]
|
||||
|
@ -20,7 +20,8 @@ class ANDROID(USBMS):
|
||||
VENDOR_ID = {
|
||||
# HTC
|
||||
0x0bb4 : { 0x0c02 : [0x100, 0x0227], 0x0c01 : [0x100, 0x0227], 0x0ff9
|
||||
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226]},
|
||||
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226],
|
||||
0xc92 : [0x100]},
|
||||
|
||||
# Motorola
|
||||
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
|
||||
|
@ -42,7 +42,7 @@ class CYBOOK(USBMS):
|
||||
DELETE_EXTS = ['.mbp', '.dat', '.bin', '_6090.t2b', '.thn']
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
coverdata = getattr(metadata, 'thumbnail', None)
|
||||
if coverdata and coverdata[2]:
|
||||
coverdata = coverdata[2]
|
||||
|
@ -77,7 +77,7 @@ class ALEX(N516):
|
||||
name = os.path.splitext(os.path.basename(file_abspath))[0] + '.png'
|
||||
return os.path.join(base, 'covers', name)
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
from calibre.ebooks import calibre_cover
|
||||
from calibre.utils.magick.draw import thumbnail
|
||||
coverdata = getattr(metadata, 'thumbnail', None)
|
||||
@ -129,7 +129,7 @@ class AZBOOKA(ALEX):
|
||||
def can_handle(self, device_info, debug=False):
|
||||
return not is_alex(device_info)
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
pass
|
||||
|
||||
class EB511(USBMS):
|
||||
|
@ -22,7 +22,9 @@ class KOBO(USBMS):
|
||||
gui_name = 'Kobo Reader'
|
||||
description = _('Communicate with the Kobo Reader')
|
||||
author = 'Timothy Legge and Kovid Goyal'
|
||||
version = (1, 0, 6)
|
||||
version = (1, 0, 7)
|
||||
|
||||
dbversion = 0
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
||||
@ -92,7 +94,7 @@ class KOBO(USBMS):
|
||||
if lpath.startswith(os.sep):
|
||||
lpath = lpath[len(os.sep):]
|
||||
lpath = lpath.replace('\\', '/')
|
||||
# print "LPATH: " + lpath
|
||||
# debug_print("LPATH: ", lpath, " - Title: " , title)
|
||||
|
||||
playlist_map = {}
|
||||
|
||||
@ -112,7 +114,7 @@ class KOBO(USBMS):
|
||||
#print "Image name Normalized: " + imagename
|
||||
if imagename is not None:
|
||||
bl[idx].thumbnail = ImageWrapper(imagename)
|
||||
if ContentType != '6':
|
||||
if (ContentType != '6'and self.dbversion < 8) or (self.dbversion >= 8):
|
||||
if self.update_metadata_item(bl[idx]):
|
||||
# print 'update_metadata_item returned true'
|
||||
changed = True
|
||||
@ -120,10 +122,16 @@ class KOBO(USBMS):
|
||||
playlist_map[lpath] not in bl[idx].device_collections:
|
||||
bl[idx].device_collections.append(playlist_map[lpath])
|
||||
else:
|
||||
if ContentType == '6':
|
||||
if ContentType == '6' and self.dbversion < 8:
|
||||
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
||||
else:
|
||||
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
||||
try:
|
||||
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
||||
except:
|
||||
debug_print("prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors, \
|
||||
"mime: ", mime, "date: ", date, "ContentType: ", ContentType, "ImageID: ", ImageID)
|
||||
raise
|
||||
|
||||
# print 'Update booklist'
|
||||
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else []
|
||||
|
||||
@ -143,6 +151,13 @@ class KOBO(USBMS):
|
||||
# numrows = row[0]
|
||||
#cursor.close()
|
||||
|
||||
# Determine the database version
|
||||
# 4 - Bluetooth Kobo Rev 2 (1.4)
|
||||
# 8 - WIFI KOBO Rev 1
|
||||
cursor.execute('select version from dbversion')
|
||||
result = cursor.fetchone()
|
||||
self.dbversion = result[0]
|
||||
|
||||
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||
'ImageID, ReadStatus from content where BookID is Null'
|
||||
|
||||
@ -153,7 +168,8 @@ class KOBO(USBMS):
|
||||
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
||||
|
||||
path = self.path_from_contentid(row[3], row[5], oncard)
|
||||
mime = mime_type_ext(path_to_ext(row[3]))
|
||||
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
|
||||
# debug_print("mime:", mime)
|
||||
|
||||
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
|
||||
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7])
|
||||
@ -206,7 +222,7 @@ class KOBO(USBMS):
|
||||
cursor.close()
|
||||
|
||||
cursor = connection.cursor()
|
||||
if ContentType == 6:
|
||||
if ContentType == 6 and self.dbversion < 8:
|
||||
# Delete the shortcover_pages first
|
||||
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
|
||||
|
||||
@ -249,7 +265,7 @@ class KOBO(USBMS):
|
||||
path = self.normalize_path(path)
|
||||
# print "Delete file normalized path: " + path
|
||||
extension = os.path.splitext(path)[1]
|
||||
ContentType = self.get_content_type_from_extension(extension)
|
||||
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(path)
|
||||
|
||||
ContentID = self.contentid_from_path(path, ContentType)
|
||||
|
||||
@ -332,9 +348,14 @@ class KOBO(USBMS):
|
||||
|
||||
def contentid_from_path(self, path, ContentType):
|
||||
if ContentType == 6:
|
||||
ContentID = os.path.splitext(path)[0]
|
||||
# Remove the prefix on the file. it could be either
|
||||
ContentID = ContentID.replace(self._main_prefix, '')
|
||||
if self.dbversion < 8:
|
||||
ContentID = os.path.splitext(path)[0]
|
||||
# Remove the prefix on the file. it could be either
|
||||
ContentID = ContentID.replace(self._main_prefix, '')
|
||||
else:
|
||||
ContentID = path
|
||||
ContentID = ContentID.replace(self._main_prefix + '.kobo/kepub/', '')
|
||||
|
||||
if self._card_a_prefix is not None:
|
||||
ContentID = ContentID.replace(self._card_a_prefix, '')
|
||||
elif ContentType == 999: # HTML Files
|
||||
@ -350,6 +371,13 @@ class KOBO(USBMS):
|
||||
ContentID = ContentID.replace("\\", '/')
|
||||
return ContentID
|
||||
|
||||
def get_content_type_from_path(self, path):
|
||||
# Strictly speaking the ContentType could be 6 or 10
|
||||
# however newspapers have the same storage format
|
||||
if path.find('kepub') >= 0:
|
||||
ContentType = 6
|
||||
return ContentType
|
||||
|
||||
def get_content_type_from_extension(self, extension):
|
||||
if extension == '.kobo':
|
||||
# Kobo books do not have book files. They do have some images though
|
||||
@ -369,19 +397,22 @@ class KOBO(USBMS):
|
||||
print 'path from_contentid cardb'
|
||||
elif oncard == 'carda':
|
||||
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
|
||||
# print "SD Card: " + filename
|
||||
# print "SD Card: " + path
|
||||
else:
|
||||
if ContentType == "6":
|
||||
if ContentType == "6" and self.dbversion < 8:
|
||||
# This is a hack as the kobo files do not exist
|
||||
# but the path is required to make a unique id
|
||||
# for calibre's reference
|
||||
path = self._main_prefix + path + '.kobo'
|
||||
# print "Path: " + path
|
||||
elif (ContentType == "6" or ContentType == "10") and self.dbversion >= 8:
|
||||
path = self._main_prefix + '.kobo/kepub/' + path
|
||||
# print "Internal: " + path
|
||||
else:
|
||||
# if path.startswith("file:///mnt/onboard/"):
|
||||
path = path.replace("file:///mnt/onboard/", self._main_prefix)
|
||||
path = path.replace("/mnt/onboard/", self._main_prefix)
|
||||
# print "Internal: " + filename
|
||||
# print "Internal: " + path
|
||||
|
||||
return path
|
||||
|
||||
@ -469,7 +500,7 @@ class KOBO(USBMS):
|
||||
book.device_collections = ['Im_Reading']
|
||||
|
||||
extension = os.path.splitext(book.path)[1]
|
||||
ContentType = self.get_content_type_from_extension(extension)
|
||||
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
|
||||
|
||||
ContentID = self.contentid_from_path(book.path, ContentType)
|
||||
datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
||||
@ -505,7 +536,7 @@ class KOBO(USBMS):
|
||||
book.device_collections = ['Read']
|
||||
|
||||
extension = os.path.splitext(book.path)[1]
|
||||
ContentType = self.get_content_type_from_extension(extension)
|
||||
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
|
||||
|
||||
ContentID = self.contentid_from_path(book.path, ContentType)
|
||||
# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
||||
|
@ -102,7 +102,7 @@ class PDNOVEL(USBMS):
|
||||
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
|
||||
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
coverdata = getattr(metadata, 'thumbnail', None)
|
||||
if coverdata and coverdata[2]:
|
||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||
|
@ -36,15 +36,15 @@ class N770(USBMS):
|
||||
|
||||
class N810(N770):
|
||||
name = 'Nokia 810 Device Interface'
|
||||
gui_name = 'Nokia 810'
|
||||
description = _('Communicate with the Nokia 810 internet tablet.')
|
||||
gui_name = 'Nokia 810/900'
|
||||
description = _('Communicate with the Nokia 810/900 internet tablet.')
|
||||
|
||||
PRODUCT_ID = [0x96]
|
||||
PRODUCT_ID = [0x96, 0x1c7]
|
||||
BCD = [0x316]
|
||||
|
||||
WINDOWS_MAIN_MEM = 'N810'
|
||||
WINDOWS_MAIN_MEM = ['N810', 'N900']
|
||||
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'N810 Main Memory'
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'Nokia Tablet Main Memory'
|
||||
|
||||
class E71X(USBMS):
|
||||
|
||||
|
@ -45,7 +45,7 @@ class NOOK(USBMS):
|
||||
DELETE_EXTS = ['.jpg']
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
Image, ImageDraw
|
||||
|
@ -2,5 +2,11 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
MEDIA_XML = 'database/cache/media.xml'
|
||||
MEDIA_EXT = 'database/cache/cacheExt.xml'
|
||||
|
||||
CACHE_XML = 'Sony Reader/database/cache.xml'
|
||||
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
|
||||
|
||||
MEDIA_THUMBNAIL = 'database/thumbnail'
|
||||
CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail'
|
||||
|
||||
|
@ -9,10 +9,10 @@ Device driver for the SONY devices
|
||||
import os, time, re
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS, debug_print
|
||||
from calibre.devices.prs505 import MEDIA_XML
|
||||
from calibre.devices.prs505 import CACHE_XML
|
||||
from calibre.devices.prs505 import MEDIA_XML, MEDIA_EXT, CACHE_XML, CACHE_EXT, \
|
||||
MEDIA_THUMBNAIL, CACHE_THUMBNAIL
|
||||
from calibre.devices.prs505.sony_cache import XMLCache
|
||||
from calibre import __appname__
|
||||
from calibre import __appname__, prints
|
||||
from calibre.devices.usbms.books import CollectionsBookList
|
||||
|
||||
class PRS505(USBMS):
|
||||
@ -66,6 +66,8 @@ class PRS505(USBMS):
|
||||
plugboard = None
|
||||
plugboard_func = None
|
||||
|
||||
THUMBNAIL_HEIGHT = 200
|
||||
|
||||
def windows_filter_pnp_id(self, pnp_id):
|
||||
return '_LAUNCHER' in pnp_id
|
||||
|
||||
@ -116,20 +118,21 @@ class PRS505(USBMS):
|
||||
return fname
|
||||
|
||||
def initialize_XML_cache(self):
|
||||
paths, prefixes = {}, {}
|
||||
for prefix, path, source_id in [
|
||||
('main', MEDIA_XML, 0),
|
||||
('card_a', CACHE_XML, 1),
|
||||
('card_b', CACHE_XML, 2)
|
||||
paths, prefixes, ext_paths = {}, {}, {}
|
||||
for prefix, path, ext_path, source_id in [
|
||||
('main', MEDIA_XML, MEDIA_EXT, 0),
|
||||
('card_a', CACHE_XML, CACHE_EXT, 1),
|
||||
('card_b', CACHE_XML, CACHE_EXT, 2)
|
||||
]:
|
||||
prefix = getattr(self, '_%s_prefix'%prefix)
|
||||
if prefix is not None and os.path.exists(prefix):
|
||||
paths[source_id] = os.path.join(prefix, *(path.split('/')))
|
||||
ext_paths[source_id] = os.path.join(prefix, *(ext_path.split('/')))
|
||||
prefixes[source_id] = prefix
|
||||
d = os.path.dirname(paths[source_id])
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
return XMLCache(paths, prefixes, self.settings().use_author_sort)
|
||||
return XMLCache(paths, ext_paths, prefixes, self.settings().use_author_sort)
|
||||
|
||||
def books(self, oncard=None, end_session=True):
|
||||
debug_print('PRS505: starting fetching books for card', oncard)
|
||||
@ -174,3 +177,31 @@ class PRS505(USBMS):
|
||||
def set_plugboards(self, plugboards, pb_func):
|
||||
self.plugboards = plugboards
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||
path = path.replace('/', os.sep)
|
||||
is_main = path.startswith(self._main_prefix)
|
||||
thumbnail_dir = MEDIA_THUMBNAIL if is_main else CACHE_THUMBNAIL
|
||||
prefix = None
|
||||
if is_main:
|
||||
prefix = self._main_prefix
|
||||
else:
|
||||
if self._card_a_prefix and \
|
||||
path.startswith(self._card_a_prefix):
|
||||
prefix = self._card_a_prefix
|
||||
elif self._card_b_prefix and \
|
||||
path.startswith(self._card_b_prefix):
|
||||
prefix = self._card_b_prefix
|
||||
if prefix is None:
|
||||
prints('WARNING: Failed to find prefix for:', filepath)
|
||||
return
|
||||
thumbnail_dir = os.path.join(prefix, *thumbnail_dir.split('/'))
|
||||
|
||||
relpath = os.path.relpath(filepath, prefix)
|
||||
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
|
||||
if not os.path.exists(thumbnail_dir):
|
||||
os.makedirs(thumbnail_dir)
|
||||
with open(os.path.join(thumbnail_dir, 'main_thumbnail.jpg'), 'wb') as f:
|
||||
f.write(metadata.thumbnail[-1])
|
||||
|
||||
|
@ -9,6 +9,7 @@ import os, time
|
||||
from base64 import b64decode
|
||||
from uuid import uuid4
|
||||
from lxml import etree
|
||||
from datetime import date
|
||||
|
||||
from calibre import prints, guess_type, isbytestring
|
||||
from calibre.devices.errors import DeviceError
|
||||
@ -18,6 +19,20 @@ from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.metadata import authors_to_string, title_sort, \
|
||||
authors_to_sort_string
|
||||
|
||||
'''
|
||||
cahceExt.xml
|
||||
|
||||
Periodical identifier sample from a PRS-650:
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cacheExt xmlns="http://www.sony.com/xmlns/product/prs/device/1">
|
||||
<text conformsTo="http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0" periodicalName="The Atlantic" description="Current affairs and politics focussed on the US" publicationDate="Tue, 19 Oct 2010 00:00:00 GMT" path="database/media/books/calibre/Atlantic [Mon, 18 Oct 2010], The - calibre_1701.epub">
|
||||
<thumbnail width="167" height="217">main_thumbnail.jpg</thumbnail>
|
||||
</text>
|
||||
</cacheExt>
|
||||
|
||||
'''
|
||||
|
||||
# Utility functions {{{
|
||||
EMPTY_CARD_CACHE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@ -25,6 +40,12 @@ EMPTY_CARD_CACHE = '''\
|
||||
</cache>
|
||||
'''
|
||||
|
||||
EMPTY_EXT_CACHE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cacheExt xmlns="http://www.sony.com/xmlns/product/prs/device/1">
|
||||
</cacheExt>
|
||||
'''
|
||||
|
||||
MIME_MAP = {
|
||||
"lrf" : "application/x-sony-bbeb",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
@ -63,7 +84,7 @@ def uuid():
|
||||
|
||||
class XMLCache(object):
|
||||
|
||||
def __init__(self, paths, prefixes, use_author_sort):
|
||||
def __init__(self, paths, ext_paths, prefixes, use_author_sort):
|
||||
if DEBUG:
|
||||
debug_print('Building XMLCache...', paths)
|
||||
self.paths = paths
|
||||
@ -76,8 +97,8 @@ class XMLCache(object):
|
||||
for source_id, path in paths.items():
|
||||
if source_id == 0:
|
||||
if not os.path.exists(path):
|
||||
raise DeviceError('The SONY XML cache media.xml does not exist. Try'
|
||||
' disconnecting and reconnecting your reader.')
|
||||
raise DeviceError(('The SONY XML cache %r does not exist. Try'
|
||||
' disconnecting and reconnecting your reader.')%repr(path))
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
else:
|
||||
@ -85,14 +106,34 @@ class XMLCache(object):
|
||||
if os.access(path, os.R_OK):
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
||||
self.roots[source_id] = etree.fromstring(xml_to_unicode(
|
||||
raw, strip_encoding_pats=True, assume_utf8=True,
|
||||
verbose=DEBUG)[0],
|
||||
parser=parser)
|
||||
if self.roots[source_id] is None:
|
||||
raise Exception(('The SONY database at %s is corrupted. Try '
|
||||
raise Exception(('The SONY database at %r is corrupted. Try '
|
||||
' disconnecting and reconnecting your reader.')%path)
|
||||
|
||||
self.ext_paths, self.ext_roots = {}, {}
|
||||
for source_id, path in ext_paths.items():
|
||||
if not os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(EMPTY_EXT_CACHE)
|
||||
except:
|
||||
pass
|
||||
if os.access(path, os.W_OK):
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
self.ext_roots[source_id] = etree.fromstring(
|
||||
xml_to_unicode(f.read(),
|
||||
strip_encoding_pats=True, assume_utf8=True,
|
||||
verbose=DEBUG)[0], parser=parser)
|
||||
self.ext_paths[source_id] = path
|
||||
except:
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
||||
@ -352,12 +393,18 @@ class XMLCache(object):
|
||||
debug_print('Updating XML Cache:', i)
|
||||
root = self.record_roots[i]
|
||||
lpath_map = self.build_lpath_map(root)
|
||||
ext_root = self.ext_roots[i] if i in self.ext_roots else None
|
||||
ext_lpath_map = None
|
||||
if ext_root is not None:
|
||||
ext_lpath_map = self.build_lpath_map(ext_root)
|
||||
gtz_count = ltz_count = 0
|
||||
use_tz_var = False
|
||||
for book in booklist:
|
||||
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
||||
record = lpath_map.get(book.lpath, None)
|
||||
created = False
|
||||
if record is None:
|
||||
created = True
|
||||
record = self.create_text_record(root, i, book.lpath)
|
||||
if plugboard is not None:
|
||||
newmi = book.deepcopy_metadata()
|
||||
@ -373,6 +420,13 @@ class XMLCache(object):
|
||||
if book.device_collections is None:
|
||||
book.device_collections = []
|
||||
book.device_collections = playlist_map.get(book.lpath, [])
|
||||
|
||||
if created and ext_root is not None and \
|
||||
ext_lpath_map.get(book.lpath, None) is None:
|
||||
ext_record = self.create_ext_text_record(ext_root, i,
|
||||
book.lpath, book.thumbnail)
|
||||
self.periodicalize_book(book, ext_record)
|
||||
|
||||
debug_print('Timezone votes: %d GMT, %d LTZ, use_tz_var=%s'%
|
||||
(gtz_count, ltz_count, use_tz_var))
|
||||
self.update_playlists(i, root, booklist, collections_attributes)
|
||||
@ -386,6 +440,47 @@ class XMLCache(object):
|
||||
self.fix_ids()
|
||||
debug_print('Finished update')
|
||||
|
||||
def is_sony_periodical(self, book):
|
||||
if _('News') not in book.tags:
|
||||
return False
|
||||
if not book.lpath.lower().endswith('.epub'):
|
||||
return False
|
||||
if book.pubdate.date() < date(2010, 10, 17):
|
||||
return False
|
||||
return True
|
||||
|
||||
def periodicalize_book(self, book, record):
|
||||
if not self.is_sony_periodical(book):
|
||||
return
|
||||
record.set('conformsTo',
|
||||
"http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0")
|
||||
|
||||
record.set('description', '')
|
||||
|
||||
name = None
|
||||
if '[' in book.title:
|
||||
name = book.title.split('[')[0].strip()
|
||||
if len(name) < 4:
|
||||
name = None
|
||||
if not name:
|
||||
try:
|
||||
name = [t for t in book.tags if t != _('News')][0]
|
||||
except:
|
||||
name = None
|
||||
|
||||
if not name:
|
||||
name = book.title
|
||||
|
||||
record.set('periodicalName', name)
|
||||
|
||||
try:
|
||||
pubdate = strftime(book.pubdate.utctimetuple(),
|
||||
zone=lambda x : x)
|
||||
record.set('publicationDate', pubdate)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def rebuild_collections(self, booklist, bl_index):
|
||||
if bl_index not in self.record_roots:
|
||||
return
|
||||
@ -472,6 +567,28 @@ class XMLCache(object):
|
||||
root.append(ans)
|
||||
return ans
|
||||
|
||||
def create_ext_text_record(self, root, bl_id, lpath, thumbnail):
|
||||
namespace = root.nsmap[None]
|
||||
attrib = { 'path': lpath }
|
||||
ans = root.makeelement('{%s}text'%namespace, attrib=attrib,
|
||||
nsmap=root.nsmap)
|
||||
ans.tail = '\n'
|
||||
if len(root) > 0:
|
||||
root[-1].tail = '\n\t'
|
||||
else:
|
||||
root.text = '\n\t'
|
||||
root.append(ans)
|
||||
if thumbnail and thumbnail[-1]:
|
||||
ans.text = '\n' + '\t\t'
|
||||
t = root.makeelement('{%s}thumbnail'%namespace,
|
||||
attrib={'width':str(thumbnail[0]), 'height':str(thumbnail[1])},
|
||||
nsmap=root.nsmap)
|
||||
t.text = 'main_thumbnail.jpg'
|
||||
ans.append(t)
|
||||
t.tail = '\n\t'
|
||||
return ans
|
||||
|
||||
|
||||
def update_text_record(self, record, book, path, bl_index,
|
||||
gtz_count, ltz_count, use_tz_var):
|
||||
'''
|
||||
@ -589,6 +706,18 @@ class XMLCache(object):
|
||||
'<?xml version="1.0" encoding="UTF-8"?>')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(raw)
|
||||
|
||||
for i, path in self.ext_paths.items():
|
||||
try:
|
||||
raw = etree.tostring(self.ext_roots[i], encoding='UTF-8',
|
||||
xml_declaration=True)
|
||||
except:
|
||||
continue
|
||||
raw = raw.replace("<?xml version='1.0' encoding='UTF-8'?>",
|
||||
'<?xml version="1.0" encoding="UTF-8"?>')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(raw)
|
||||
|
||||
# }}}
|
||||
|
||||
# Utility methods {{{
|
||||
|
@ -5,8 +5,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import dbus
|
||||
import os
|
||||
import dbus, os
|
||||
|
||||
def node_mountpoint(node):
|
||||
|
||||
@ -56,15 +55,6 @@ class UDisks(object):
|
||||
parent = device_node_path
|
||||
while parent[-1] in '0123456789':
|
||||
parent = parent[:-1]
|
||||
devices = [str(x) for x in self.main.EnumerateDeviceFiles()]
|
||||
for d in devices:
|
||||
if d.startswith(parent) and d != parent:
|
||||
try:
|
||||
self.unmount(d)
|
||||
except:
|
||||
import traceback
|
||||
print 'Failed to unmount:', d
|
||||
traceback.print_exc()
|
||||
d = self.device(parent)
|
||||
d.DriveEject([])
|
||||
|
||||
@ -76,13 +66,19 @@ def eject(node_path):
|
||||
u = UDisks()
|
||||
u.eject(node_path)
|
||||
|
||||
def umount(node_path):
|
||||
u = UDisks()
|
||||
u.unmount(node_path)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
dev = sys.argv[1]
|
||||
print 'Testing with node', dev
|
||||
u = UDisks()
|
||||
print 'Mounted at:', u.mount(dev)
|
||||
print 'Ejecting'
|
||||
print 'Unmounting'
|
||||
u.unmount(dev)
|
||||
print 'Ejecting:'
|
||||
u.eject(dev)
|
||||
|
||||
|
||||
|
@ -99,6 +99,13 @@ class CollectionsBookList(BookList):
|
||||
def supports_collections(self):
|
||||
return True
|
||||
|
||||
def in_category_sort_rules(self, attr):
|
||||
sorts = tweaks['sony_collection_sorting_rules']
|
||||
for attrs,sortattr in sorts:
|
||||
if attr in attrs or '*' in attrs:
|
||||
return sortattr
|
||||
return None
|
||||
|
||||
def compute_category_name(self, attr, category, field_meta):
|
||||
renames = tweaks['sony_collection_renaming_rules']
|
||||
attr_name = renames.get(attr, None)
|
||||
@ -116,6 +123,7 @@ class CollectionsBookList(BookList):
|
||||
from calibre.devices.usbms.driver import debug_print
|
||||
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
|
||||
debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
|
||||
debug_print('Sorting rules:', tweaks['sony_collection_sorting_rules'])
|
||||
|
||||
# Complexity: we can use renaming rules only when using automatic
|
||||
# management. Otherwise we don't always have the metadata to make the
|
||||
@ -171,6 +179,7 @@ class CollectionsBookList(BookList):
|
||||
else:
|
||||
val = [val]
|
||||
|
||||
sort_attr = self.in_category_sort_rules(attr)
|
||||
for category in val:
|
||||
is_series = False
|
||||
if doing_dc:
|
||||
@ -199,22 +208,41 @@ class CollectionsBookList(BookList):
|
||||
|
||||
if cat_name not in collections:
|
||||
collections[cat_name] = {}
|
||||
if is_series:
|
||||
if use_renaming_rules and sort_attr:
|
||||
sort_val = book.get(sort_attr, None)
|
||||
collections[cat_name][lpath] = \
|
||||
(book, sort_val, book.get('title_sort', 'zzzz'))
|
||||
elif is_series:
|
||||
if doing_dc:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get('series_index', sys.maxint))
|
||||
(book, book.get('series_index', sys.maxint), '')
|
||||
else:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get(attr+'_index', sys.maxint))
|
||||
(book, book.get(attr+'_index', sys.maxint), '')
|
||||
else:
|
||||
if lpath not in collections[cat_name]:
|
||||
collections[cat_name][lpath] = \
|
||||
(book, book.get('title_sort', 'zzzz'))
|
||||
(book, book.get('title_sort', 'zzzz'), '')
|
||||
# Sort collections
|
||||
result = {}
|
||||
|
||||
def none_cmp(xx, yy):
|
||||
x = xx[1]
|
||||
y = yy[1]
|
||||
if x is None and y is None:
|
||||
return cmp(xx[2], yy[2])
|
||||
if x is None:
|
||||
return 1
|
||||
if y is None:
|
||||
return -1
|
||||
c = cmp(x, y)
|
||||
if c != 0:
|
||||
return c
|
||||
return cmp(xx[2], yy[2])
|
||||
|
||||
for category, lpaths in collections.items():
|
||||
books = lpaths.values()
|
||||
books.sort(cmp=lambda x,y:cmp(x[1], y[1]))
|
||||
books.sort(cmp=none_cmp)
|
||||
result[category] = [x[0] for x in books]
|
||||
return result
|
||||
|
||||
|
@ -523,7 +523,8 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
devnodes.append(node)
|
||||
|
||||
devnodes += list(repeat(None, 3))
|
||||
ans = tuple(['/dev/'+x if ok.get(x, False) else None for x in devnodes[:3]])
|
||||
ans = ['/dev/'+x if ok.get(x, False) else None for x in devnodes[:3]]
|
||||
ans.sort(key=lambda x: x[5:] if x else 'zzzzz')
|
||||
return self.linux_swap_drives(ans)
|
||||
|
||||
def linux_swap_drives(self, drives):
|
||||
@ -732,24 +733,36 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
pass
|
||||
|
||||
def eject_linux(self):
|
||||
try:
|
||||
from calibre.devices.udisks import eject
|
||||
return eject(self._linux_main_device_node)
|
||||
except:
|
||||
pass
|
||||
drives = self.find_device_nodes()
|
||||
from calibre.devices.udisks import eject, umount
|
||||
drives = [d for d in self.find_device_nodes() if d]
|
||||
for d in drives:
|
||||
try:
|
||||
umount(d)
|
||||
except:
|
||||
pass
|
||||
failures = False
|
||||
for d in drives:
|
||||
try:
|
||||
eject(d)
|
||||
except Exception, e:
|
||||
print 'Udisks eject call for:', d, 'failed:'
|
||||
print '\t', e
|
||||
failures = True
|
||||
|
||||
if not failures:
|
||||
return
|
||||
|
||||
for drive in drives:
|
||||
if drive:
|
||||
cmd = 'calibre-mount-helper'
|
||||
if getattr(sys, 'frozen_path', False):
|
||||
cmd = os.path.join(sys.frozen_path, cmd)
|
||||
cmd = [cmd, 'eject']
|
||||
mp = getattr(self, "_linux_mount_map", {}).get(drive,
|
||||
'dummy/')[:-1]
|
||||
try:
|
||||
subprocess.Popen(cmd + [drive, mp]).wait()
|
||||
except:
|
||||
pass
|
||||
cmd = 'calibre-mount-helper'
|
||||
if getattr(sys, 'frozen_path', False):
|
||||
cmd = os.path.join(sys.frozen_path, cmd)
|
||||
cmd = [cmd, 'eject']
|
||||
mp = getattr(self, "_linux_mount_map", {}).get(drive,
|
||||
'dummy/')[:-1]
|
||||
try:
|
||||
subprocess.Popen(cmd + [drive, mp]).wait()
|
||||
except:
|
||||
pass
|
||||
|
||||
def eject(self):
|
||||
if islinux:
|
||||
|
@ -186,7 +186,8 @@ class USBMS(CLI, Device):
|
||||
self.put_file(infile, filepath, replace_file=True)
|
||||
try:
|
||||
self.upload_cover(os.path.dirname(filepath),
|
||||
os.path.splitext(os.path.basename(filepath))[0], mdata)
|
||||
os.path.splitext(os.path.basename(filepath))[0],
|
||||
mdata, filepath)
|
||||
except: # Failure to upload cover is not catastrophic
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@ -197,14 +198,15 @@ class USBMS(CLI, Device):
|
||||
debug_print('USBMS: finished uploading %d books'%(len(files)))
|
||||
return zip(paths, cycle([on_card]))
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
'''
|
||||
Upload book cover to the device. Default implementation does nothing.
|
||||
|
||||
:param path: the full path were the associated book is located.
|
||||
:param filename: the name of the book file without the extension.
|
||||
:param path: The full path to the directory where the associated book is located.
|
||||
:param filename: The name of the book file without the extension.
|
||||
:param metadata: metadata belonging to the book. Use metadata.thumbnail
|
||||
for cover
|
||||
:param filepath: The full path to the ebook file
|
||||
|
||||
'''
|
||||
pass
|
||||
|
@ -14,7 +14,7 @@ from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.date import parse_date
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
from calibre import extract, walk
|
||||
from calibre import extract, walk, isbytestring, filesystem_encoding
|
||||
from calibre.constants import __version__
|
||||
|
||||
DEBUG_README=u'''
|
||||
@ -77,6 +77,10 @@ class Plumber(object):
|
||||
:param input: Path to input file.
|
||||
:param output: Path to output file/directory
|
||||
'''
|
||||
if isbytestring(input):
|
||||
input = input.decode(filesystem_encoding)
|
||||
if isbytestring(output):
|
||||
output = output.decode(filesystem_encoding)
|
||||
self.original_input_arg = input
|
||||
self.input = os.path.abspath(input)
|
||||
self.output = os.path.abspath(output)
|
||||
|
@ -15,22 +15,30 @@ def rules(stylesheets):
|
||||
if r.type == r.STYLE_RULE:
|
||||
yield r
|
||||
|
||||
def initialize_container(path_to_container, opf_name='metadata.opf'):
|
||||
def initialize_container(path_to_container, opf_name='metadata.opf',
|
||||
extra_entries=[]):
|
||||
'''
|
||||
Create an empty EPUB document, with a default skeleton.
|
||||
'''
|
||||
CONTAINER='''\
|
||||
rootfiles = ''
|
||||
for path, mimetype, _ in extra_entries:
|
||||
rootfiles += u'<rootfile full-path="{0}" media-type="{1}"/>'.format(
|
||||
path, mimetype)
|
||||
CONTAINER = u'''\
|
||||
<?xml version="1.0"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="%s" media-type="application/oebps-package+xml"/>
|
||||
<rootfile full-path="{0}" media-type="application/oebps-package+xml"/>
|
||||
{extra_entries}
|
||||
</rootfiles>
|
||||
</container>
|
||||
'''%opf_name
|
||||
'''.format(opf_name, extra_entries=rootfiles).encode('utf-8')
|
||||
zf = ZipFile(path_to_container, 'w')
|
||||
zf.writestr('mimetype', 'application/epub+zip', compression=ZIP_STORED)
|
||||
zf.writestr('META-INF/', '', 0700)
|
||||
zf.writestr('META-INF/container.xml', CONTAINER)
|
||||
for path, _, data in extra_entries:
|
||||
zf.writestr(path, data)
|
||||
return zf
|
||||
|
||||
|
||||
|
@ -108,6 +108,27 @@ class EPUBInput(InputFormatPlugin):
|
||||
open('calibre_raster_cover.jpg', 'wb').write(
|
||||
renderer)
|
||||
|
||||
def find_opf(self):
|
||||
def attr(n, attr):
|
||||
for k, v in n.attrib.items():
|
||||
if k.endswith(attr):
|
||||
return v
|
||||
try:
|
||||
with open('META-INF/container.xml') as f:
|
||||
root = etree.fromstring(f.read())
|
||||
for r in root.xpath('//*[local-name()="rootfile"]'):
|
||||
if attr(r, 'media-type') != "application/oebps-package+xml":
|
||||
continue
|
||||
path = attr(r, 'full-path')
|
||||
if not path:
|
||||
continue
|
||||
path = os.path.join(os.getcwdu(), *path.split('/'))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def convert(self, stream, options, file_ext, log, accelerators):
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
from calibre import walk
|
||||
@ -116,12 +137,13 @@ class EPUBInput(InputFormatPlugin):
|
||||
zf = ZipFile(stream)
|
||||
zf.extractall(os.getcwd())
|
||||
encfile = os.path.abspath(os.path.join('META-INF', 'encryption.xml'))
|
||||
opf = None
|
||||
for f in walk(u'.'):
|
||||
if f.lower().endswith('.opf') and '__MACOSX' not in f and \
|
||||
not os.path.basename(f).startswith('.'):
|
||||
opf = os.path.abspath(f)
|
||||
break
|
||||
opf = self.find_opf()
|
||||
if opf is None:
|
||||
for f in walk(u'.'):
|
||||
if f.lower().endswith('.opf') and '__MACOSX' not in f and \
|
||||
not os.path.basename(f).startswith('.'):
|
||||
opf = os.path.abspath(f)
|
||||
break
|
||||
path = getattr(stream, 'name', 'stream')
|
||||
|
||||
if opf is None:
|
||||
|
@ -106,6 +106,7 @@ class EPUBOutput(OutputFormatPlugin):
|
||||
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
||||
|
||||
|
||||
|
||||
def workaround_webkit_quirks(self): # {{{
|
||||
from calibre.ebooks.oeb.base import XPath
|
||||
for x in self.oeb.spine:
|
||||
@ -183,6 +184,12 @@ class EPUBOutput(OutputFormatPlugin):
|
||||
|
||||
with TemporaryDirectory('_epub_output') as tdir:
|
||||
from calibre.customize.ui import plugin_for_output_format
|
||||
metadata_xml = None
|
||||
extra_entries = []
|
||||
if self.is_periodical:
|
||||
from calibre.ebooks.epub.periodical import sony_metadata
|
||||
metadata_xml, atom_xml = sony_metadata(oeb)
|
||||
extra_entries = [('atom.xml', 'application/atom+xml', atom_xml)]
|
||||
oeb_output = plugin_for_output_format('oeb')
|
||||
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
|
||||
opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
|
||||
@ -194,10 +201,14 @@ class EPUBOutput(OutputFormatPlugin):
|
||||
encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid)
|
||||
|
||||
from calibre.ebooks.epub import initialize_container
|
||||
epub = initialize_container(output_path, os.path.basename(opf))
|
||||
epub = initialize_container(output_path, os.path.basename(opf),
|
||||
extra_entries=extra_entries)
|
||||
epub.add_dir(tdir)
|
||||
if encryption is not None:
|
||||
epub.writestr('META-INF/encryption.xml', encryption)
|
||||
if metadata_xml is not None:
|
||||
epub.writestr('META-INF/metadata.xml',
|
||||
metadata_xml.encode('utf-8'))
|
||||
if opts.extract_to is not None:
|
||||
if os.path.exists(opts.extract_to):
|
||||
shutil.rmtree(opts.extract_to)
|
||||
|
173
src/calibre/ebooks/epub/periodical.py
Normal file
173
src/calibre/ebooks/epub/periodical.py
Normal file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre import strftime, prepare_string_for_xml as xml
|
||||
|
||||
SONY_METADATA = u'''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:prs="http://xmlns.sony.net/e-book/prs/">
|
||||
<rdf:Description rdf:about="">
|
||||
<dc:title>{title}</dc:title>
|
||||
<dc:publisher>{publisher}</dc:publisher>
|
||||
<dcterms:alternative>{short_title}</dcterms:alternative>
|
||||
<dcterms:issued>{issue_date}</dcterms:issued>
|
||||
<dc:language>{language}</dc:language>
|
||||
<dcterms:conformsTo rdf:resource="http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0"/>
|
||||
<dcterms:type rdf:resource="http://xmlns.sony.net/e-book/prs/datatype/newspaper"/>
|
||||
<dcterms:type rdf:resource="http://xmlns.sony.net/e-book/prs/datatype/periodical"/>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
'''
|
||||
|
||||
SONY_ATOM = u'''\
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||
xmlns:prs="http://xmlns.sony.net/e-book/prs/"
|
||||
xmlns:media="http://video.search.yahoo.com/mrss"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<title>{short_title}</title>
|
||||
<updated>{updated}</updated>
|
||||
<id>{id}</id>
|
||||
{entries}
|
||||
</feed>
|
||||
'''
|
||||
|
||||
SONY_ATOM_SECTION = u'''\
|
||||
<entry rdf:ID="{title}">
|
||||
<title>{title}</title>
|
||||
<link href="{href}"/>
|
||||
<id>{id}</id>
|
||||
<updated>{updated}</updated>
|
||||
<summary>{desc}</summary>
|
||||
<category term="{short_title}/{title}"
|
||||
scheme="http://xmlns.sony.net/e-book/terms/" label="{title}"/>
|
||||
<dc:type xsi:type="prs:datatype">newspaper/section</dc:type>
|
||||
<dcterms:isReferencedBy rdf:resource=""/>
|
||||
</entry>
|
||||
'''
|
||||
|
||||
SONY_ATOM_ENTRY = u'''\
|
||||
<entry>
|
||||
<title>{title}</title>
|
||||
<author><name>{author}</name></author>
|
||||
<link href="{href}"/>
|
||||
<id>{id}</id>
|
||||
<updated>{updated}</updated>
|
||||
<summary>{desc}</summary>
|
||||
<category term="{short_title}/{section_title}"
|
||||
scheme="http://xmlns.sony.net/e-book/terms/" label="{section_title}"/>
|
||||
<dcterms:extent xsi:type="prs:word-count">{word_count}</dcterms:extent>
|
||||
<dc:type xsi:type="prs:datatype">newspaper/article</dc:type>
|
||||
<dcterms:isReferencedBy rdf:resource="#{section_title}"/>
|
||||
</entry>
|
||||
'''
|
||||
|
||||
def sony_metadata(oeb):
|
||||
m = oeb.metadata
|
||||
title = short_title = unicode(m.title[0])
|
||||
publisher = __appname__ + ' ' + __version__
|
||||
try:
|
||||
pt = unicode(oeb.metadata.publication_type[0])
|
||||
short_title = u':'.join(pt.split(':')[2:])
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
date = unicode(m.date[0]).split('T')[0]
|
||||
except:
|
||||
date = strftime('%Y-%m-%d')
|
||||
try:
|
||||
language = unicode(m.language[0]).replace('_', '-')
|
||||
except:
|
||||
language = 'en'
|
||||
short_title = xml(short_title, True)
|
||||
|
||||
metadata = SONY_METADATA.format(title=xml(title),
|
||||
short_title=short_title,
|
||||
publisher=xml(publisher), issue_date=xml(date),
|
||||
language=xml(language))
|
||||
|
||||
updated = strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
def cal_id(x):
|
||||
for k, v in x.attrib.items():
|
||||
if k.endswith('scheme') and v == 'uuid':
|
||||
return True
|
||||
|
||||
try:
|
||||
base_id = unicode(list(filter(cal_id, m.identifier))[0])
|
||||
except:
|
||||
base_id = str(uuid4())
|
||||
|
||||
entries = []
|
||||
seen_titles = set([])
|
||||
for i, section in enumerate(oeb.toc):
|
||||
if not section.href:
|
||||
continue
|
||||
secid = 'section%d'%i
|
||||
sectitle = section.title
|
||||
if not sectitle:
|
||||
sectitle = _('Unknown')
|
||||
d = 1
|
||||
bsectitle = sectitle
|
||||
while sectitle in seen_titles:
|
||||
sectitle = bsectitle + ' ' + str(d)
|
||||
d += 1
|
||||
seen_titles.add(sectitle)
|
||||
sectitle = xml(sectitle, True)
|
||||
secdesc = section.description
|
||||
if not secdesc:
|
||||
secdesc = ''
|
||||
secdesc = xml(secdesc)
|
||||
entries.append(SONY_ATOM_SECTION.format(title=sectitle,
|
||||
href=section.href, id=xml(base_id)+'/'+secid,
|
||||
short_title=short_title, desc=secdesc, updated=updated))
|
||||
|
||||
for j, article in enumerate(section):
|
||||
if not article.href:
|
||||
continue
|
||||
atitle = article.title
|
||||
btitle = atitle
|
||||
d = 1
|
||||
while atitle in seen_titles:
|
||||
atitle = btitle + ' ' + str(d)
|
||||
d += 1
|
||||
|
||||
auth = article.author if article.author else ''
|
||||
desc = section.description
|
||||
if not desc:
|
||||
desc = ''
|
||||
aid = 'article%d'%j
|
||||
|
||||
entries.append(SONY_ATOM_ENTRY.format(
|
||||
title=xml(atitle),
|
||||
author=xml(auth),
|
||||
updated=updated,
|
||||
desc=desc,
|
||||
short_title=short_title,
|
||||
section_title=sectitle,
|
||||
href=article.href,
|
||||
word_count=str(1),
|
||||
id=xml(base_id)+'/'+secid+'/'+aid
|
||||
))
|
||||
|
||||
atom = SONY_ATOM.format(short_title=short_title,
|
||||
entries='\n\n'.join(entries), updated=updated,
|
||||
id=xml(base_id)).encode('utf-8')
|
||||
|
||||
return metadata, atom
|
||||
|
@ -43,7 +43,7 @@ class SafeFormat(TemplateFormatter):
|
||||
b = self.book.get_user_metadata(key, False)
|
||||
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
||||
v = ''
|
||||
elif b and b['datatype'] == 'float' and b.get(key, 0.0) == 0.0:
|
||||
elif b and b['datatype'] == 'float' and self.book.get(key, 0.0) == 0.0:
|
||||
v = ''
|
||||
else:
|
||||
ign, v = self.book.format_field(key.lower(), series_with_index=False)
|
||||
@ -501,7 +501,7 @@ class Metadata(object):
|
||||
if key.startswith('#') and key.endswith('_index'):
|
||||
tkey = key[:-6] # strip the _index
|
||||
cmeta = self.get_user_metadata(tkey, make_copy=False)
|
||||
if cmeta['datatype'] == 'series':
|
||||
if cmeta and cmeta['datatype'] == 'series':
|
||||
if self.get(tkey):
|
||||
res = self.get_extra(tkey)
|
||||
return (unicode(cmeta['name']+'_index'),
|
||||
|
@ -382,11 +382,13 @@ class Guide(ResourceCollection): # {{{
|
||||
|
||||
class MetadataField(object):
|
||||
|
||||
def __init__(self, name, is_dc=True, formatter=None, none_is=None):
|
||||
def __init__(self, name, is_dc=True, formatter=None, none_is=None,
|
||||
renderer=lambda x: unicode(x)):
|
||||
self.name = name
|
||||
self.is_dc = is_dc
|
||||
self.formatter = formatter
|
||||
self.none_is = none_is
|
||||
self.renderer = renderer
|
||||
|
||||
def __real_get__(self, obj, type=None):
|
||||
ans = obj.get_metadata_element(self.name)
|
||||
@ -418,7 +420,7 @@ class MetadataField(object):
|
||||
return
|
||||
if elem is None:
|
||||
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
|
||||
obj.set_text(elem, unicode(val))
|
||||
obj.set_text(elem, self.renderer(val))
|
||||
|
||||
|
||||
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
|
||||
@ -489,10 +491,11 @@ class OPF(object): # {{{
|
||||
series = MetadataField('series', is_dc=False)
|
||||
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
|
||||
rating = MetadataField('rating', is_dc=False, formatter=int)
|
||||
pubdate = MetadataField('date', formatter=parse_date)
|
||||
pubdate = MetadataField('date', formatter=parse_date,
|
||||
renderer=isoformat)
|
||||
publication_type = MetadataField('publication_type', is_dc=False)
|
||||
timestamp = MetadataField('timestamp', is_dc=False,
|
||||
formatter=parse_date)
|
||||
formatter=parse_date, renderer=isoformat)
|
||||
|
||||
|
||||
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True,
|
||||
@ -826,11 +829,10 @@ class OPF(object): # {{{
|
||||
|
||||
def fset(self, val):
|
||||
matches = self.isbn_path(self.metadata)
|
||||
if val is None:
|
||||
if matches:
|
||||
for x in matches:
|
||||
x.getparent().remove(x)
|
||||
return
|
||||
if not val:
|
||||
for x in matches:
|
||||
x.getparent().remove(x)
|
||||
return
|
||||
if not matches:
|
||||
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'}
|
||||
matches = [self.create_metadata_element('identifier',
|
||||
@ -987,11 +989,14 @@ class OPF(object): # {{{
|
||||
def smart_update(self, mi, replace_metadata=False):
|
||||
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
||||
'publisher', 'series', 'series_index', 'rating',
|
||||
'isbn', 'language', 'tags', 'category', 'comments',
|
||||
'isbn', 'tags', 'category', 'comments',
|
||||
'pubdate'):
|
||||
val = getattr(mi, attr, None)
|
||||
if val is not None and val != [] and val != (None, None):
|
||||
setattr(self, attr, val)
|
||||
lang = getattr(mi, 'language', None)
|
||||
if lang and lang != 'und':
|
||||
self.language = lang
|
||||
temp = self.to_book_metadata()
|
||||
temp.smart_update(mi, replace_metadata=replace_metadata)
|
||||
self._user_metadata_ = temp.get_all_user_metadata(True)
|
||||
|
@ -42,11 +42,10 @@ class MOBIOutput(OutputFormatPlugin):
|
||||
])
|
||||
|
||||
def check_for_periodical(self):
|
||||
if self.oeb.metadata.publication_type and \
|
||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:'):
|
||||
self.periodicalize_toc()
|
||||
self.check_for_masthead()
|
||||
self.opts.mobi_periodical = True
|
||||
if self.is_periodical:
|
||||
self.periodicalize_toc()
|
||||
self.check_for_masthead()
|
||||
self.opts.mobi_periodical = True
|
||||
else:
|
||||
self.opts.mobi_periodical = False
|
||||
|
||||
|
@ -6,8 +6,6 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import cStringIO
|
||||
|
||||
from calibre import fit_image
|
||||
|
||||
class RescaleImages(object):
|
||||
@ -19,13 +17,7 @@ class RescaleImages(object):
|
||||
self.rescale(qt=is_ok_to_use_qt())
|
||||
|
||||
def rescale(self, qt=True):
|
||||
from PyQt4.Qt import QImage, Qt
|
||||
from calibre.gui2 import pixmap_to_data
|
||||
try:
|
||||
from PIL import Image as PILImage
|
||||
PILImage
|
||||
except ImportError:
|
||||
import Image as PILImage
|
||||
from calibre.utils.magick.draw import Image
|
||||
|
||||
is_image_collection = getattr(self.opts, 'is_image_collection', False)
|
||||
|
||||
@ -35,6 +27,7 @@ class RescaleImages(object):
|
||||
page_width, page_height = self.opts.dest.width, self.opts.dest.height
|
||||
page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi/72.
|
||||
page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72.
|
||||
|
||||
for item in self.oeb.manifest:
|
||||
if item.media_type.startswith('image'):
|
||||
ext = item.media_type.split('/')[-1].upper()
|
||||
@ -44,42 +37,25 @@ class RescaleImages(object):
|
||||
|
||||
raw = item.data
|
||||
if not raw: continue
|
||||
if qt:
|
||||
img = QImage(10, 10, QImage.Format_ARGB32_Premultiplied)
|
||||
try:
|
||||
if not img.loadFromData(raw): continue
|
||||
except:
|
||||
continue
|
||||
width, height = img.width(), img.height()
|
||||
else:
|
||||
f = cStringIO.StringIO(raw)
|
||||
try:
|
||||
im = PILImage.open(f)
|
||||
except IOError:
|
||||
continue
|
||||
width, height = im.size
|
||||
|
||||
try:
|
||||
img = Image()
|
||||
img.load(raw)
|
||||
except:
|
||||
continue
|
||||
width, height = img.size
|
||||
|
||||
|
||||
scaled, new_width, new_height = fit_image(width, height,
|
||||
page_width, page_height)
|
||||
if scaled:
|
||||
data = None
|
||||
self.log('Rescaling image from %dx%d to %dx%d'%(
|
||||
width, height, new_width, new_height), item.href)
|
||||
if qt:
|
||||
img = img.scaled(new_width, new_height,
|
||||
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||
data = pixmap_to_data(img, format=ext)
|
||||
try:
|
||||
img.size = (new_width, new_height)
|
||||
data = img.export(ext.lower())
|
||||
except:
|
||||
self.log.exception('Failed to rescale image')
|
||||
else:
|
||||
try:
|
||||
im = im.resize((int(new_width), int(new_height)), PILImage.ANTIALIAS)
|
||||
of = cStringIO.StringIO()
|
||||
im.convert('RGB').save(of, ext)
|
||||
data = of.getvalue()
|
||||
except:
|
||||
self.log.exception('Failed to rescale image')
|
||||
if data is not None:
|
||||
item.data = data
|
||||
item.unload_data_from_memory()
|
||||
|
||||
|
@ -514,7 +514,7 @@ class FileDialog(QObject):
|
||||
if f and os.path.exists(f):
|
||||
self.selected_files.append(f)
|
||||
else:
|
||||
opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.DirectoryOnly else QFileDialog.Option()
|
||||
opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.Directory else QFileDialog.Option()
|
||||
f = unicode(QFileDialog.getExistingDirectory(parent, title, initial_dir, opts))
|
||||
if os.path.exists(f):
|
||||
self.selected_files.append(f)
|
||||
@ -534,7 +534,7 @@ class FileDialog(QObject):
|
||||
|
||||
def choose_dir(window, name, title, default_dir='~'):
|
||||
fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
|
||||
parent=window, name=name, mode=QFileDialog.DirectoryOnly,
|
||||
parent=window, name=name, mode=QFileDialog.Directory,
|
||||
default_dir=default_dir)
|
||||
dir = fd.get_files()
|
||||
if dir:
|
||||
|
@ -9,7 +9,6 @@ from PyQt4.Qt import Qt
|
||||
|
||||
from calibre.gui2 import Dispatcher
|
||||
from calibre.gui2.tools import fetch_scheduled_recipe
|
||||
from calibre.utils.config import dynamic
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
class FetchNewsAction(InterfaceAction):
|
||||
@ -60,9 +59,9 @@ class FetchNewsAction(InterfaceAction):
|
||||
return self.gui.job_exception(job)
|
||||
id = self.gui.library_view.model().add_news(pt.name, arg)
|
||||
self.gui.library_view.model().reset()
|
||||
sync = dynamic.get('news_to_be_synced', set([]))
|
||||
sync = self.gui.news_to_be_synced
|
||||
sync.add(id)
|
||||
dynamic.set('news_to_be_synced', sync)
|
||||
self.gui.news_to_be_synced = sync
|
||||
self.scheduler.recipe_downloaded(arg)
|
||||
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
||||
self.gui.email_news(id)
|
||||
|
@ -429,7 +429,38 @@ class BulkBase(Base):
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
|
||||
class BulkBool(BulkBase, Bool):
|
||||
pass
|
||||
|
||||
def get_initial_value(self, book_ids):
|
||||
value = None
|
||||
for book_id in book_ids:
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||
val = False
|
||||
if value is not None and value != val:
|
||||
return None
|
||||
value = val
|
||||
return value
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QComboBox(parent)]
|
||||
w = self.widgets[1]
|
||||
items = [_('Yes'), _('No'), _('Undefined')]
|
||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||
for icon, text in zip(icons, items):
|
||||
w.addItem(QIcon(icon), text)
|
||||
|
||||
def setter(self, val):
|
||||
val = {None: 2, False: 1, True: 0}[val]
|
||||
self.widgets[1].setCurrentIndex(val)
|
||||
|
||||
def commit(self, book_ids, notify=False):
|
||||
val = self.gui_val
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val:
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||
val = False
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
|
||||
class BulkInt(BulkBase, Int):
|
||||
pass
|
||||
|
@ -1102,12 +1102,35 @@ class DeviceMixin(object): # {{{
|
||||
self.status_bar.show_message(_('Sending catalogs to device.'), 5000)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def news_to_be_synced(self):
|
||||
doc = 'Set of ids to be sent to device'
|
||||
def fget(self):
|
||||
ans = []
|
||||
try:
|
||||
ans = self.library_view.model().db.prefs.get('news_to_be_synced',
|
||||
[])
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return set(ans)
|
||||
|
||||
def fset(self, ids):
|
||||
try:
|
||||
self.library_view.model().db.prefs.set('news_to_be_synced',
|
||||
list(ids))
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return property(fget=fget, fset=fset, doc=doc)
|
||||
|
||||
|
||||
def sync_news(self, send_ids=None, do_auto_convert=True):
|
||||
if self.device_connected:
|
||||
del_on_upload = config['delete_news_from_library_on_upload']
|
||||
settings = self.device_manager.device.settings()
|
||||
ids = list(dynamic.get('news_to_be_synced', set([]))) if send_ids is None else send_ids
|
||||
ids = list(self.news_to_be_synced) if send_ids is None else send_ids
|
||||
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
|
||||
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(
|
||||
ids, settings.format_map,
|
||||
@ -1139,7 +1162,7 @@ class DeviceMixin(object): # {{{
|
||||
for f in files:
|
||||
f.deleted_after_upload = del_on_upload
|
||||
if not files:
|
||||
dynamic.set('news_to_be_synced', set([]))
|
||||
self.news_to_be_synced = set([])
|
||||
return
|
||||
metadata = self.library_view.model().metadata_for(ids)
|
||||
names = []
|
||||
@ -1153,7 +1176,7 @@ class DeviceMixin(object): # {{{
|
||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
||||
'rb').read())
|
||||
dynamic.set('news_to_be_synced', set([]))
|
||||
self.news_to_be_synced = set([])
|
||||
if config['upload_news_to_device'] and files:
|
||||
remove = ids if del_on_upload else []
|
||||
space = { self.location_manager.free[0] : None,
|
||||
@ -1347,8 +1370,9 @@ class DeviceMixin(object): # {{{
|
||||
# If it does not, then do it here.
|
||||
if not self.set_books_in_library(self.booklists(), reset=True):
|
||||
self.upload_booklists()
|
||||
self.book_on_device(None, reset=True)
|
||||
self.refresh_ondevice()
|
||||
with self.library_view.preserve_selected_books:
|
||||
self.book_on_device(None, reset=True)
|
||||
self.refresh_ondevice()
|
||||
|
||||
view = self.card_a_view if on_card == 'carda' else \
|
||||
self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||
|
@ -90,10 +90,15 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
row = row.row()
|
||||
if row == self.current_row:
|
||||
return
|
||||
info = self.view.model().get_book_info(row)
|
||||
if info is None:
|
||||
# Indicates books was deleted from library, or row numbers have
|
||||
# changed
|
||||
return
|
||||
|
||||
self.previous_button.setEnabled(False if row == 0 else True)
|
||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||
self.current_row = row
|
||||
info = self.view.model().get_book_info(row)
|
||||
self.setWindowTitle(info[_('Title')])
|
||||
self.title.setText('<b>'+info.pop(_('Title')))
|
||||
comments = info.pop(_('Comments'), '')
|
||||
|
@ -3,11 +3,16 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
||||
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
||||
QLineEdit
|
||||
QLineEdit, Qt
|
||||
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.library.check_library import CheckLibrary, CHECKS
|
||||
from calibre.library.database2 import delete_file, delete_tree
|
||||
from calibre import prints
|
||||
|
||||
class Item(QTreeWidgetItem):
|
||||
pass
|
||||
@ -24,24 +29,25 @@ class CheckLibraryDialog(QDialog):
|
||||
self.setLayout(self._layout)
|
||||
|
||||
self.log = QTreeWidget(self)
|
||||
self.log.itemChanged.connect(self.item_changed)
|
||||
self._layout.addWidget(self.log)
|
||||
|
||||
self.check = QPushButton(_('Run the check'))
|
||||
self.check = QPushButton(_('&Run the check'))
|
||||
self.check.setDefault(False)
|
||||
self.check.clicked.connect(self.run_the_check)
|
||||
self.copy = QPushButton(_('Copy to clipboard'))
|
||||
self.copy = QPushButton(_('Copy &to clipboard'))
|
||||
self.copy.setDefault(False)
|
||||
self.copy.clicked.connect(self.copy_to_clipboard)
|
||||
self.ok = QPushButton('&Done')
|
||||
self.ok.setDefault(True)
|
||||
self.ok.clicked.connect(self.accept)
|
||||
self.cancel = QPushButton('&Cancel')
|
||||
self.cancel.setDefault(False)
|
||||
self.cancel.clicked.connect(self.reject)
|
||||
self.delete = QPushButton('Delete &marked')
|
||||
self.delete.setDefault(False)
|
||||
self.delete.clicked.connect(self.delete_marked)
|
||||
self.bbox = QDialogButtonBox(self)
|
||||
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
||||
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
|
||||
self.bbox.addButton(self.cancel, QDialogButtonBox.RejectRole)
|
||||
self.bbox.addButton(self.delete, QDialogButtonBox.ActionRole)
|
||||
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
||||
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
|
||||
|
||||
h = QHBoxLayout()
|
||||
@ -83,35 +89,70 @@ class CheckLibraryDialog(QDialog):
|
||||
plaintext = []
|
||||
|
||||
def builder(tree, checker, check):
|
||||
attr = check[0]
|
||||
attr, h, checkable = check
|
||||
list = getattr(checker, attr, None)
|
||||
if list is None:
|
||||
return
|
||||
|
||||
h = check[1]
|
||||
tl = Item([h])
|
||||
for problem in list:
|
||||
it = Item()
|
||||
if checkable:
|
||||
it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
|
||||
it.setCheckState(1, False)
|
||||
else:
|
||||
it.setFlags(Qt.ItemIsEnabled)
|
||||
it.setText(0, problem[0])
|
||||
it.setText(1, problem[1])
|
||||
p = ', '.join(problem[2])
|
||||
it.setText(2, p)
|
||||
tl.addChild(it)
|
||||
plaintext.append(','.join([h, problem[0], problem[1], p]))
|
||||
self.all_items.append(it)
|
||||
plaintext.append(','.join([h, problem[0], problem[1]]))
|
||||
tree.addTopLevelItem(tl)
|
||||
|
||||
t = self.log
|
||||
t.clear()
|
||||
t.setColumnCount(3);
|
||||
t.setHeaderLabels([_('Name'), _('Path from library'), _('Additional Information')])
|
||||
t.setColumnCount(2);
|
||||
t.setHeaderLabels([_('Name'), _('Path from library')])
|
||||
self.all_items = []
|
||||
for check in CHECKS:
|
||||
builder(t, checker, check)
|
||||
|
||||
t.setColumnWidth(0, 200)
|
||||
t.setColumnWidth(1, 400)
|
||||
|
||||
self.delete.setEnabled(False)
|
||||
self.text_results = '\n'.join(plaintext)
|
||||
|
||||
def item_changed(self, item, column):
|
||||
for it in self.all_items:
|
||||
if it.checkState(1):
|
||||
self.delete.setEnabled(True)
|
||||
return
|
||||
|
||||
def delete_marked(self):
|
||||
if not confirm('<p>'+_('The marked files and folders will be '
|
||||
'<b>permanently deleted</b>. Are you sure?')
|
||||
+'</p>', 'check_library_editor_delete', self):
|
||||
return
|
||||
|
||||
# Sort the paths in reverse length order so that we can be sure that
|
||||
# if an item is in another item, the sub-item will be deleted first.
|
||||
items = sorted(self.all_items,
|
||||
key=lambda x: len(x.text(1)),
|
||||
reverse=True)
|
||||
for it in items:
|
||||
if it.checkState(1):
|
||||
try:
|
||||
p = os.path.join(self.db.library_path ,unicode(it.text(1)))
|
||||
if os.path.isdir(p):
|
||||
delete_tree(p)
|
||||
else:
|
||||
delete_file(p)
|
||||
except:
|
||||
prints('failed to delete',
|
||||
os.path.join(self.db.library_path,
|
||||
unicode(it.text(1))))
|
||||
self.run_the_check()
|
||||
|
||||
def copy_to_clipboard(self):
|
||||
QApplication.clipboard().setText(self.text_results)
|
||||
|
||||
|
@ -190,7 +190,8 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||
if self.model.rowCount() < 1:
|
||||
info_dialog(self, _('No metadata found'),
|
||||
_('No metadata found, try adjusting the title and author '
|
||||
'or the ISBN key.')).exec_()
|
||||
'and/or removing the ISBN.')).exec_()
|
||||
self.reject()
|
||||
return
|
||||
|
||||
self.matches.setModel(self.model)
|
||||
|
@ -16,6 +16,7 @@ from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||
from calibre.utils.config import dynamic
|
||||
from calibre.utils.titlecase import titlecase
|
||||
|
||||
class MyBlockingBusy(QDialog):
|
||||
|
||||
@ -50,6 +51,7 @@ class MyBlockingBusy(QDialog):
|
||||
self.start()
|
||||
|
||||
self.args = args
|
||||
self.series_start_value = None
|
||||
self.db = db
|
||||
self.ids = ids
|
||||
self.error = None
|
||||
@ -115,7 +117,7 @@ class MyBlockingBusy(QDialog):
|
||||
aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
||||
new_title = authors_to_string(aum)
|
||||
if do_title_case:
|
||||
new_title = new_title.title()
|
||||
new_title = titlecase(new_title)
|
||||
self.db.set_title(id, new_title, notify=False)
|
||||
title_set = True
|
||||
if title:
|
||||
@ -123,7 +125,7 @@ class MyBlockingBusy(QDialog):
|
||||
self.db.set_authors(id, new_authors, notify=False)
|
||||
if do_title_case and not title_set:
|
||||
title = self.db.title(id, index_is_id=True)
|
||||
self.db.set_title(id, title.title(), notify=False)
|
||||
self.db.set_title(id, titlecase(title), notify=False)
|
||||
if au:
|
||||
self.db.set_authors(id, string_to_authors(au), notify=False)
|
||||
elif self.current_phase == 2:
|
||||
@ -147,8 +149,10 @@ class MyBlockingBusy(QDialog):
|
||||
|
||||
if do_series:
|
||||
if do_series_restart:
|
||||
next = series_start_value
|
||||
series_start_value += 1
|
||||
if self.series_start_value is None:
|
||||
self.series_start_value = series_start_value
|
||||
next = self.series_start_value
|
||||
self.series_start_value += 1
|
||||
else:
|
||||
next = self.db.get_next_series_num_for(series)
|
||||
self.db.set_series(id, series, notify=False, commit=False)
|
||||
@ -179,7 +183,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
s_r_functions = { '' : lambda x: x,
|
||||
_('Lower Case') : lambda x: x.lower(),
|
||||
_('Upper Case') : lambda x: x.upper(),
|
||||
_('Title Case') : lambda x: x.title(),
|
||||
_('Title Case') : lambda x: titlecase(x),
|
||||
}
|
||||
|
||||
s_r_match_modes = [ _('Character match'),
|
||||
|
@ -374,6 +374,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
if isinstance(index, int):
|
||||
index = self.index(index, 0)
|
||||
data = self.current_changed(index, None, False)
|
||||
if data is None:
|
||||
return data
|
||||
row = index.row()
|
||||
data[_('Title')] = self.db.title(row)
|
||||
au = self.db.authors(row)
|
||||
@ -783,18 +785,22 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.db.set_rating(id, val)
|
||||
elif column == 'series':
|
||||
val = val.strip()
|
||||
pat = re.compile(r'\[([.0-9]+)\]')
|
||||
match = pat.search(val)
|
||||
if match is not None:
|
||||
self.db.set_series_index(id, float(match.group(1)))
|
||||
val = pat.sub('', val).strip()
|
||||
elif val:
|
||||
if tweaks['series_index_auto_increment'] == 'next':
|
||||
ni = self.db.get_next_series_num_for(val)
|
||||
if ni != 1:
|
||||
self.db.set_series_index(id, ni)
|
||||
if val:
|
||||
if not val:
|
||||
self.db.set_series(id, val)
|
||||
self.db.set_series_index(id, 1.0)
|
||||
else:
|
||||
pat = re.compile(r'\[([.0-9]+)\]')
|
||||
match = pat.search(val)
|
||||
if match is not None:
|
||||
self.db.set_series_index(id, float(match.group(1)))
|
||||
val = pat.sub('', val).strip()
|
||||
elif val:
|
||||
if tweaks['series_index_auto_increment'] == 'next':
|
||||
ni = self.db.get_next_series_num_for(val)
|
||||
if ni != 1:
|
||||
self.db.set_series_index(id, ni)
|
||||
if val:
|
||||
self.db.set_series(id, val)
|
||||
elif column == 'timestamp':
|
||||
if val.isNull() or not val.isValid():
|
||||
return False
|
||||
|
@ -22,6 +22,26 @@ from calibre.gui2.library import DEFAULT_SORT
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre import force_unicode
|
||||
|
||||
class PreserveSelection(object): # {{{
|
||||
|
||||
'''
|
||||
Save the set of selected books at enter time. If at exit time there are no
|
||||
selected books, restore the previous selection.
|
||||
'''
|
||||
|
||||
def __init__(self, view):
|
||||
self.view = view
|
||||
self.selected_ids = []
|
||||
|
||||
def __enter__(self):
|
||||
self.selected_ids = self.view.get_selected_ids()
|
||||
|
||||
def __exit__(self, *args):
|
||||
current = self.view.get_selected_ids()
|
||||
if not current:
|
||||
self.view.select_rows(self.selected_ids, using_ids=True)
|
||||
# }}}
|
||||
|
||||
class BooksView(QTableView): # {{{
|
||||
|
||||
files_dropped = pyqtSignal(object)
|
||||
@ -58,6 +78,7 @@ class BooksView(QTableView): # {{{
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSortingEnabled(True)
|
||||
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
||||
self.preserve_selected_books = PreserveSelection(self)
|
||||
|
||||
# {{{ Column Header setup
|
||||
self.can_add_columns = True
|
||||
@ -613,6 +634,16 @@ class BooksView(QTableView): # {{{
|
||||
sel.select(m.index(row, 0), m.index(row, max_col))
|
||||
sm.select(sel, sm.ClearAndSelect)
|
||||
|
||||
def get_selected_ids(self):
|
||||
ans = []
|
||||
m = self.model()
|
||||
for idx in self.selectedIndexes():
|
||||
r = idx.row()
|
||||
i = m.id(r)
|
||||
if i not in ans:
|
||||
ans.append(i)
|
||||
return ans
|
||||
|
||||
def close(self):
|
||||
self._model.close()
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
<ui version="4.0" >
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow" >
|
||||
<property name="geometry" >
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
@ -9,75 +10,51 @@
|
||||
<height>701</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle" >
|
||||
<property name="windowTitle">
|
||||
<string>LRF Viewer</string>
|
||||
</property>
|
||||
<property name="windowIcon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/images/viewer.png</normaloff>:/images/viewer.png</iconset>
|
||||
</property>
|
||||
<widget class="QWidget" name="central_widget" >
|
||||
<property name="geometry" >
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>39</y>
|
||||
<width>601</width>
|
||||
<height>662</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" >
|
||||
<property name="margin" >
|
||||
<widget class="QWidget" name="central_widget">
|
||||
<layout class="QVBoxLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack" >
|
||||
<property name="currentIndex" >
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="viewer_page" >
|
||||
<property name="geometry" >
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>601</width>
|
||||
<height>662</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QGridLayout" >
|
||||
<property name="margin" >
|
||||
<widget class="QWidget" name="viewer_page">
|
||||
<layout class="QGridLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0" >
|
||||
<widget class="BookView" name="graphics_view" >
|
||||
<property name="mouseTracking" >
|
||||
<item row="0" column="0">
|
||||
<widget class="BookView" name="graphics_view">
|
||||
<property name="mouseTracking">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="bar_page" >
|
||||
<property name="geometry" >
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>601</width>
|
||||
<height>701</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" >
|
||||
<widget class="QWidget" name="bar_page">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation" >
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0" >
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
@ -86,34 +63,34 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_2" >
|
||||
<property name="frameShape" >
|
||||
<widget class="QFrame" name="frame_2">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow" >
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" >
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progress_bar" >
|
||||
<property name="maximum" >
|
||||
<widget class="QProgressBar" name="progress_bar">
|
||||
<property name="maximum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="value" >
|
||||
<property name="value">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="progress_label" >
|
||||
<property name="font" >
|
||||
<widget class="QLabel" name="progress_label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Parsing LRF file</string>
|
||||
</property>
|
||||
</widget>
|
||||
@ -123,10 +100,10 @@
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation" >
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0" >
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
@ -140,93 +117,85 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="tool_bar" >
|
||||
<property name="geometry" >
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>601</width>
|
||||
<height>39</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle" >
|
||||
<widget class="QToolBar" name="tool_bar">
|
||||
<property name="windowTitle">
|
||||
<string>LRF Viewer toolbar</string>
|
||||
</property>
|
||||
<property name="allowedAreas" >
|
||||
<property name="allowedAreas">
|
||||
<set>Qt::AllToolBarAreas</set>
|
||||
</property>
|
||||
<attribute name="toolBarArea" >
|
||||
<enum>TopToolBarArea</enum>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>Qt::TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak" >
|
||||
<bool>true</bool>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="action_back" />
|
||||
<addaction name="action_forward" />
|
||||
<addaction name="separator" />
|
||||
<addaction name="action_open_ebook" />
|
||||
<addaction name="action_configure" />
|
||||
<addaction name="separator" />
|
||||
<addaction name="action_previous_page" />
|
||||
<addaction name="action_next_page" />
|
||||
<addaction name="separator" />
|
||||
<addaction name="action_back"/>
|
||||
<addaction name="action_forward"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_open_ebook"/>
|
||||
<addaction name="action_configure"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_previous_page"/>
|
||||
<addaction name="action_next_page"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<action name="action_next_page" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_next_page">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/next.png</normaloff>:/images/next.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Next Page</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_previous_page" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_previous_page">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/previous.png</normaloff>:/images/previous.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Previous Page</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_back" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_back">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Back</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_forward" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_forward">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Forward</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_next_match" >
|
||||
<property name="text" >
|
||||
<action name="action_next_match">
|
||||
<property name="text">
|
||||
<string>Next match</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_open_ebook" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_open_ebook">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Open ebook</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_configure" >
|
||||
<property name="icon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<action name="action_configure">
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/config.png</normaloff>:/images/config.png</iconset>
|
||||
</property>
|
||||
<property name="text" >
|
||||
<property name="text">
|
||||
<string>Configure</string>
|
||||
</property>
|
||||
</action>
|
||||
@ -239,7 +208,7 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc" />
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -365,6 +365,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
||||
except:
|
||||
olddb = None
|
||||
db = LibraryDatabase2(newloc)
|
||||
if self.content_server is not None:
|
||||
self.content_server.set_database(db)
|
||||
self.library_path = newloc
|
||||
self.book_on_device(None, reset=True)
|
||||
db.set_book_on_device_func(self.book_on_device)
|
||||
|
@ -716,6 +716,9 @@ View an ebook.
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
# Ensure viewer can continue to function if GUI is closed
|
||||
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
|
||||
|
||||
parser = option_parser()
|
||||
opts, args = parser.parse_args(args)
|
||||
pid = os.fork() if False and (islinux or isfreebsd) else -1
|
||||
|
@ -108,7 +108,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>LeftToolBarArea</enum>
|
||||
<enum>Qt::LeftToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
@ -136,7 +136,7 @@
|
||||
</widget>
|
||||
<widget class="QToolBar" name="tool_bar2">
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
<enum>Qt::TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
|
@ -380,7 +380,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
field_count = 3
|
||||
else:
|
||||
try:
|
||||
qd = parse_date(query)
|
||||
qd = parse_date(query, as_utc=False)
|
||||
except:
|
||||
raise ParseException(query, len(query), 'Date conversion error', self)
|
||||
if '-' in query:
|
||||
@ -816,6 +816,10 @@ class SortKeyGenerator(object):
|
||||
if val is None:
|
||||
val = ''
|
||||
val = val.lower()
|
||||
|
||||
elif dt == 'bool':
|
||||
val = {True: 1, False: 2, None: 3}.get(val, 3)
|
||||
|
||||
yield val
|
||||
|
||||
# }}}
|
||||
|
@ -14,14 +14,14 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
||||
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
|
||||
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
|
||||
|
||||
CHECKS = [('invalid_titles', _('Invalid titles')),
|
||||
('extra_titles', _('Extra titles')),
|
||||
('invalid_authors', _('Invalid authors')),
|
||||
('extra_authors', _('Extra authors')),
|
||||
('missing_formats', _('Missing book formats')),
|
||||
('extra_formats', _('Extra book formats')),
|
||||
('extra_files', _('Unknown files in books')),
|
||||
('failed_folders', _('Folders raising exception'))
|
||||
CHECKS = [('invalid_titles', _('Invalid titles'), True),
|
||||
('extra_titles', _('Extra titles'), True),
|
||||
('invalid_authors', _('Invalid authors'), True),
|
||||
('extra_authors', _('Extra authors'), True),
|
||||
('missing_formats', _('Missing book formats'), False),
|
||||
('extra_formats', _('Extra book formats'), True),
|
||||
('extra_files', _('Unknown files in books'), True),
|
||||
('failed_folders', _('Folders raising exception'), False)
|
||||
]
|
||||
|
||||
|
||||
@ -41,7 +41,6 @@ class CheckLibrary(object):
|
||||
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
|
||||
|
||||
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
|
||||
self.bad_ext_pat = re.compile(r'[^a-z0-9]+')
|
||||
|
||||
self.dirs = []
|
||||
self.book_dirs = []
|
||||
@ -78,7 +77,7 @@ class CheckLibrary(object):
|
||||
auth_path = os.path.join(lib, auth_dir)
|
||||
# First check: author must be a directory
|
||||
if not os.path.isdir(auth_path):
|
||||
self.invalid_authors.append((auth_dir, auth_dir, []))
|
||||
self.invalid_authors.append((auth_dir, auth_dir))
|
||||
continue
|
||||
|
||||
self.potential_authors[auth_dir] = {}
|
||||
@ -93,7 +92,7 @@ class CheckLibrary(object):
|
||||
m = self.db_id_regexp.search(title_dir)
|
||||
# Second check: title must have an ID and must be a directory
|
||||
if m is None or not os.path.isdir(title_path):
|
||||
self.invalid_titles.append((auth_dir, db_path, [title_dir]))
|
||||
self.invalid_titles.append((auth_dir, db_path))
|
||||
continue
|
||||
|
||||
id = m.group(1)
|
||||
@ -101,12 +100,12 @@ class CheckLibrary(object):
|
||||
if self.is_case_sensitive:
|
||||
if int(id) not in self.all_ids or \
|
||||
db_path not in self.all_dbpaths:
|
||||
self.extra_titles.append((title_dir, db_path, []))
|
||||
self.extra_titles.append((title_dir, db_path))
|
||||
continue
|
||||
else:
|
||||
if int(id) not in self.all_ids or \
|
||||
db_path.lower() not in self.all_lc_dbpaths:
|
||||
self.extra_titles.append((title_dir, db_path, []))
|
||||
self.extra_titles.append((title_dir, db_path))
|
||||
continue
|
||||
|
||||
# Record the book to check its formats
|
||||
@ -115,7 +114,7 @@ class CheckLibrary(object):
|
||||
|
||||
# Fourth check: author directories that contain no titles
|
||||
if not found_titles:
|
||||
self.extra_authors.append((auth_dir, auth_dir, []))
|
||||
self.extra_authors.append((auth_dir, auth_dir))
|
||||
|
||||
for x in self.book_dirs:
|
||||
try:
|
||||
@ -132,9 +131,7 @@ class CheckLibrary(object):
|
||||
ext = ext[1:].lower()
|
||||
if ext in EBOOK_EXTENSIONS:
|
||||
return True
|
||||
if self.bad_ext_pat.search(ext) is not None:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def process_book(self, lib, book_info):
|
||||
(db_path, title_dir, book_id) = book_info
|
||||
@ -148,18 +145,18 @@ class CheckLibrary(object):
|
||||
if self.is_case_sensitive:
|
||||
unknowns = frozenset(filenames-formats-NORMALS)
|
||||
# Check: any books that aren't formats or normally there?
|
||||
if unknowns:
|
||||
self.extra_files.append((title_dir, db_path, unknowns))
|
||||
for u in unknowns:
|
||||
self.extra_files.append((title_dir, os.path.join(db_path, u)))
|
||||
|
||||
# Check: any book formats that should be there?
|
||||
missing = book_formats - formats
|
||||
if missing:
|
||||
self.missing_formats.append((title_dir, db_path, missing))
|
||||
for m in missing:
|
||||
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||
|
||||
# Check: any book formats that shouldn't be there?
|
||||
extra = formats - book_formats - NORMALS
|
||||
if extra:
|
||||
self.extra_formats.append((title_dir, db_path, extra))
|
||||
for e in extra:
|
||||
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||
else:
|
||||
def lc_map(fnames, fset):
|
||||
m = {}
|
||||
@ -171,19 +168,16 @@ class CheckLibrary(object):
|
||||
formats_lc = frozenset([f.lower() for f in formats])
|
||||
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
|
||||
# Check: any books that aren't formats or normally there?
|
||||
if unknowns:
|
||||
self.extra_files.append((title_dir, db_path,
|
||||
lc_map(filenames, unknowns)))
|
||||
for f in lc_map(filenames, unknowns):
|
||||
self.extra_files.append((title_dir, os.path.join(db_path, f)))
|
||||
|
||||
book_formats_lc = frozenset([f.lower() for f in book_formats])
|
||||
# Check: any book formats that should be there?
|
||||
missing = book_formats_lc - formats_lc
|
||||
if missing:
|
||||
self.missing_formats.append((title_dir, db_path,
|
||||
lc_map(book_formats, missing)))
|
||||
for m in lc_map(book_formats, missing):
|
||||
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||
|
||||
# Check: any book formats that shouldn't be there?
|
||||
extra = formats_lc - book_formats_lc - NORMALS
|
||||
if extra:
|
||||
self.extra_formats.append((title_dir, db_path,
|
||||
lc_map(formats, extra)))
|
||||
for e in lc_map(formats, extra):
|
||||
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||
|
@ -943,11 +943,11 @@ def command_check_library(args, dbpath):
|
||||
return
|
||||
if opts.csv:
|
||||
for i in list:
|
||||
print check[1] + ',' + i[0] + ',' + i[1] + ',' + '|'.join(i[2])
|
||||
print check[1] + ',' + i[0] + ',' + i[1]
|
||||
else:
|
||||
print check[1]
|
||||
for i in list:
|
||||
print ' %-30.30s - %-30.30s - %s'%(i[0], i[1], ', '.join(i[2]))
|
||||
print ' %-40.40s - %-40.40s'%(i[0], i[1])
|
||||
|
||||
db = LibraryDatabase2(dbpath)
|
||||
checker = CheckLibrary(dbpath, db)
|
||||
|
@ -653,17 +653,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.dirtied(book_ids)
|
||||
|
||||
def get_metadata_for_dump(self, idx, remove_from_dirtied=True):
|
||||
path, mi = (None, None)
|
||||
try:
|
||||
path = os.path.join(self.abspath(idx, index_is_id=True), 'metadata.opf')
|
||||
mi = self.get_metadata(idx, index_is_id=True)
|
||||
# Always set cover to cover.jpg. Even if cover doesn't exist,
|
||||
# no harm done. This way no need to call dirtied when
|
||||
# cover is set/removed
|
||||
mi.cover = 'cover.jpg'
|
||||
# While a book is being created, the path is empty. Don't bother to
|
||||
# try to write the opf, because it will go to the wrong folder.
|
||||
if self.path(idx, index_is_id=True):
|
||||
path = os.path.join(self.abspath(idx, index_is_id=True), 'metadata.opf')
|
||||
mi = self.get_metadata(idx, index_is_id=True)
|
||||
# Always set cover to cover.jpg. Even if cover doesn't exist,
|
||||
# no harm done. This way no need to call dirtied when
|
||||
# cover is set/removed
|
||||
mi.cover = 'cover.jpg'
|
||||
except:
|
||||
# This almost certainly means that the book has been deleted while
|
||||
# the backup operation sat in the queue.
|
||||
path, mi = (None, None)
|
||||
pass
|
||||
|
||||
try:
|
||||
# clear the dirtied indicator. The user must put it back if
|
||||
@ -748,10 +752,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return False
|
||||
|
||||
def find_identical_books(self, mi):
|
||||
fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
|
||||
fuzzy_title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||
[
|
||||
(r'[\[\](){}<>\'";,:#]', ''),
|
||||
(r'^(the|a|an) ', ''),
|
||||
(tweaks.get('title_sort_articles', r'^(a|the|an)\s+'), ''),
|
||||
(r'[-._]', ' '),
|
||||
(r'\s+', ' ')
|
||||
]
|
||||
|
@ -71,9 +71,17 @@ class Restore(Thread):
|
||||
|
||||
if self.conflicting_custom_cols:
|
||||
ans += '\n\n'
|
||||
ans += 'The following custom columns were not fully restored:\n'
|
||||
ans += 'The following custom columns have conflicting definitions ' \
|
||||
'and were not fully restored:\n'
|
||||
for x in self.conflicting_custom_cols:
|
||||
ans += '\t#'+x+'\n'
|
||||
ans += '\tused:\t%s, %s, %s, %s\n'%(self.custom_columns[x][1],
|
||||
self.custom_columns[x][2],
|
||||
self.custom_columns[x][3],
|
||||
self.custom_columns[x][5])
|
||||
for coldef in self.conflicting_custom_cols[x]:
|
||||
ans += '\tother:\t%s, %s, %s, %s\n'%(coldef[1], coldef[2],
|
||||
coldef[3], coldef[5])
|
||||
|
||||
if self.mismatched_dirs:
|
||||
ans += '\n\n'
|
||||
@ -152,7 +160,7 @@ class Restore(Thread):
|
||||
|
||||
def create_cc_metadata(self):
|
||||
self.books.sort(key=itemgetter('timestamp'))
|
||||
m = {}
|
||||
self.custom_columns = {}
|
||||
fields = ('label', 'name', 'datatype', 'is_multiple', 'is_editable',
|
||||
'display')
|
||||
for b in self.books:
|
||||
@ -168,16 +176,17 @@ class Restore(Thread):
|
||||
if len(args) == len(fields):
|
||||
# TODO: Do series type columns need special handling?
|
||||
label = cfm['label']
|
||||
if label in m and args != m[label]:
|
||||
if label in self.custom_columns and args != self.custom_columns[label]:
|
||||
if label not in self.conflicting_custom_cols:
|
||||
self.conflicting_custom_cols[label] = set([m[label]])
|
||||
self.conflicting_custom_cols[label].add(args)
|
||||
m[cfm['label']] = args
|
||||
self.conflicting_custom_cols[label] = []
|
||||
if self.custom_columns[label] not in self.conflicting_custom_cols[label]:
|
||||
self.conflicting_custom_cols[label].append(self.custom_columns[label])
|
||||
self.custom_columns[label] = args
|
||||
|
||||
db = RestoreDatabase(self.library_path)
|
||||
self.progress_callback(None, len(m))
|
||||
if len(m):
|
||||
for i,args in enumerate(m.values()):
|
||||
self.progress_callback(None, len(self.custom_columns))
|
||||
if len(self.custom_columns):
|
||||
for i,args in enumerate(self.custom_columns.values()):
|
||||
db.create_custom_column(*args)
|
||||
self.progress_callback(_('creating custom column ')+args[0], i+1)
|
||||
db.conn.close()
|
||||
|
@ -131,15 +131,14 @@ class SafeFormat(TemplateFormatter):
|
||||
self.vformat(b['display']['composite_template'], [], kwargs)
|
||||
return self.composite_values[key]
|
||||
if key in kwargs:
|
||||
return kwargs[key].replace('/', '_').replace('\\', '_')
|
||||
val = kwargs[key]
|
||||
return val.replace('/', '_').replace('\\', '_')
|
||||
return ''
|
||||
except:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
return key
|
||||
|
||||
safe_formatter = SafeFormat()
|
||||
|
||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||
to_lowercase=False):
|
||||
@ -173,17 +172,22 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
custom_metadata = mi.get_all_user_metadata(make_copy=False)
|
||||
for key in custom_metadata:
|
||||
if key in format_args:
|
||||
cm = custom_metadata[key]
|
||||
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
|
||||
if custom_metadata[key]['datatype'] == 'series':
|
||||
if cm['datatype'] == 'series':
|
||||
format_args[key] = tsfmt(format_args[key])
|
||||
if key+'_index' in format_args:
|
||||
format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
|
||||
elif custom_metadata[key]['datatype'] == 'datetime':
|
||||
elif cm['datatype'] == 'datetime':
|
||||
format_args[key] = strftime(timefmt, format_args[key].timetuple())
|
||||
elif custom_metadata[key]['datatype'] == 'bool':
|
||||
elif cm['datatype'] == 'bool':
|
||||
format_args[key] = _('yes') if format_args[key] else _('no')
|
||||
|
||||
components = safe_formatter.safe_format(template, format_args,
|
||||
elif cm['datatype'] in ['int', 'float']:
|
||||
if format_args[key] != 0:
|
||||
format_args[key] = unicode(format_args[key])
|
||||
else:
|
||||
format_args[key] = ''
|
||||
components = SafeFormat().safe_format(template, format_args,
|
||||
'G_C-EXCEPTION!', mi)
|
||||
components = [x.strip() for x in components.split('/') if x.strip()]
|
||||
components = [sanitize_func(x) for x in components if x]
|
||||
|
@ -10,6 +10,7 @@ import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.process.plugins import SimplePlugin
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.date import fromtimestamp
|
||||
@ -54,16 +55,43 @@ class DispatchController(object): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class BonJour(SimplePlugin):
|
||||
|
||||
def __init__(self, engine, port=8080):
|
||||
SimplePlugin.__init__(self, engine)
|
||||
self.port = port
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||
self.port, {'path':'/stanza'})
|
||||
except:
|
||||
import traceback
|
||||
cherrypy.log.error('Failed to start BonJour:')
|
||||
cherrypy.log.error(traceback.format_exc())
|
||||
|
||||
start.priority = 90
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
stop_zeroconf()
|
||||
except:
|
||||
import traceback
|
||||
cherrypy.log.error('Failed to stop BonJour:')
|
||||
cherrypy.log.error(traceback.format_exc())
|
||||
|
||||
|
||||
stop.priority = 10
|
||||
|
||||
cherrypy.engine.bonjour = BonJour(cherrypy.engine)
|
||||
|
||||
|
||||
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||
BrowseServer):
|
||||
|
||||
server_name = __appname__ + '/' + __version__
|
||||
|
||||
def __init__(self, db, opts, embedded=False, show_tracebacks=True):
|
||||
self.db = db
|
||||
for item in self.db:
|
||||
item
|
||||
break
|
||||
self.opts = opts
|
||||
self.embedded = embedded
|
||||
self.state_callback = None
|
||||
@ -71,7 +99,14 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||
map(int, self.opts.max_cover.split('x'))
|
||||
path = P('content_server')
|
||||
self.build_time = fromtimestamp(os.stat(path).st_mtime)
|
||||
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
||||
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
||||
|
||||
cherrypy.engine.bonjour.port = opts.port
|
||||
|
||||
Cache.__init__(self)
|
||||
|
||||
self.set_database(db)
|
||||
|
||||
cherrypy.config.update({
|
||||
'log.screen' : opts.develop,
|
||||
'engine.autoreload_on' : opts.develop,
|
||||
@ -97,18 +132,28 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
|
||||
}
|
||||
|
||||
sr = getattr(opts, 'restriction', None)
|
||||
sr = db.prefs.get('cs_restriction', '') if sr is None else sr
|
||||
self.set_search_restriction(sr)
|
||||
|
||||
self.is_running = False
|
||||
self.exception = None
|
||||
self.setup_loggers()
|
||||
cherrypy.engine.bonjour.subscribe()
|
||||
|
||||
def set_database(self, db):
|
||||
self.db = db
|
||||
sr = getattr(self.opts, 'restriction', None)
|
||||
sr = db.prefs.get('cs_restriction', '') if sr is None else sr
|
||||
self.set_search_restriction(sr)
|
||||
|
||||
def graceful(self):
|
||||
cherrypy.engine.graceful()
|
||||
|
||||
def set_search_restriction(self, restriction):
|
||||
self.search_restriction_name = restriction
|
||||
if restriction:
|
||||
self.search_restriction = 'search:"%s"'%restriction
|
||||
else:
|
||||
self.search_restriction = ''
|
||||
self.reset_caches()
|
||||
|
||||
def setup_loggers(self):
|
||||
access_file = log_access_file
|
||||
@ -140,7 +185,6 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||
root_conf['request.dispatch'] = d.dispatcher
|
||||
self.config['/'] = root_conf
|
||||
|
||||
self.setup_loggers()
|
||||
cherrypy.tree.mount(root=None, config=self.config)
|
||||
try:
|
||||
try:
|
||||
@ -154,24 +198,14 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||
cherrypy.engine.start()
|
||||
|
||||
self.is_running = True
|
||||
try:
|
||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||
self.opts.port, {'path':'/stanza'})
|
||||
except:
|
||||
import traceback
|
||||
cherrypy.log.error('Failed to start BonJour:')
|
||||
cherrypy.log.error(traceback.format_exc())
|
||||
#if hasattr(cherrypy.engine, 'signal_handler'):
|
||||
# cherrypy.engine.signal_handler.subscribe()
|
||||
|
||||
cherrypy.engine.block()
|
||||
except Exception, e:
|
||||
self.exception = e
|
||||
finally:
|
||||
self.is_running = False
|
||||
try:
|
||||
stop_zeroconf()
|
||||
except:
|
||||
import traceback
|
||||
cherrypy.log.error('Failed to stop BonJour:')
|
||||
cherrypy.log.error(traceback.format_exc())
|
||||
try:
|
||||
if callable(self.state_callback):
|
||||
self.state_callback(self.is_running)
|
||||
|
@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import operator, os, json
|
||||
from binascii import hexlify, unhexlify
|
||||
from urllib import quote, unquote
|
||||
|
||||
import cherrypy
|
||||
|
||||
@ -115,7 +116,10 @@ def render_rating(rating, container='span', prefix=None): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
def get_category_items(category, items, db, datatype): # {{{
|
||||
def get_category_items(category, items, restriction, datatype): # {{{
|
||||
|
||||
if category == 'search':
|
||||
items = [x for x in items if x.name != restriction]
|
||||
|
||||
def item(i):
|
||||
templ = (u'<div title="{4}" class="category-item">'
|
||||
@ -136,7 +140,7 @@ def get_category_items(category, items, db, datatype): # {{{
|
||||
q = i.category
|
||||
if not q:
|
||||
q = category
|
||||
href = '/browse/matches/%s/%s'%(q, id_)
|
||||
href = '/browse/matches/%s/%s'%(quote(q), quote(id_))
|
||||
return templ.format(xml(name), rating,
|
||||
xml(desc), xml(href), rstring)
|
||||
|
||||
@ -164,6 +168,9 @@ class Endpoint(object): # {{{
|
||||
sort_val = cookie[eself.sort_cookie_name].value
|
||||
kwargs[eself.sort_kwarg] = sort_val
|
||||
|
||||
# Remove AJAX caching disabling jquery workaround arg
|
||||
kwargs.pop('_', None)
|
||||
|
||||
ans = func(self, *args, **kwargs)
|
||||
cherrypy.response.headers['Content-Type'] = eself.mimetype
|
||||
updated = self.db.last_modified()
|
||||
@ -298,6 +305,7 @@ class BrowseServer(object):
|
||||
category_meta = self.db.field_metadata
|
||||
cats = [
|
||||
(_('Newest'), 'newest', 'forward.png'),
|
||||
(_('All books'), 'allbooks', 'book.png'),
|
||||
]
|
||||
|
||||
def getter(x):
|
||||
@ -329,7 +337,7 @@ class BrowseServer(object):
|
||||
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
|
||||
'<span class="label">{0}</span>'
|
||||
'<span class="url">/browse/category/{1}</span></li>')
|
||||
.format(xml(x, True), xml(y), xml(_('Browse books by')),
|
||||
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
||||
src='/browse/icon/'+z)
|
||||
for x, y, z in cats]
|
||||
|
||||
@ -369,7 +377,8 @@ class BrowseServer(object):
|
||||
|
||||
if len(items) <= self.opts.max_opds_ungrouped_items:
|
||||
script = 'false'
|
||||
items = get_category_items(category, items, self.db, datatype)
|
||||
items = get_category_items(category, items,
|
||||
self.search_restriction_name, datatype)
|
||||
else:
|
||||
getter = lambda x: unicode(getattr(x, 'sort', x.name))
|
||||
starts = set([])
|
||||
@ -439,7 +448,8 @@ class BrowseServer(object):
|
||||
entries.append(x)
|
||||
|
||||
sort = self.browse_sort_categories(entries, sort)
|
||||
entries = get_category_items(category, entries, self.db, datatype)
|
||||
entries = get_category_items(category, entries,
|
||||
self.search_restriction_name, datatype)
|
||||
return json.dumps(entries, ensure_ascii=False)
|
||||
|
||||
|
||||
@ -450,6 +460,8 @@ class BrowseServer(object):
|
||||
ans = self.browse_toplevel()
|
||||
elif category == 'newest':
|
||||
raise cherrypy.InternalRedirect('/browse/matches/newest/dummy')
|
||||
elif category == 'allbooks':
|
||||
raise cherrypy.InternalRedirect('/browse/matches/allbooks/dummy')
|
||||
else:
|
||||
ans = self.browse_category(category, category_sort)
|
||||
|
||||
@ -473,32 +485,40 @@ class BrowseServer(object):
|
||||
|
||||
@Endpoint(sort_type='list')
|
||||
def browse_matches(self, category=None, cid=None, list_sort=None):
|
||||
if list_sort:
|
||||
list_sort = unquote(list_sort)
|
||||
if not cid:
|
||||
raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid)
|
||||
categories = self.categories_cache()
|
||||
|
||||
if category not in categories and category != 'newest':
|
||||
if category not in categories and \
|
||||
category not in ('newest', 'allbooks'):
|
||||
raise cherrypy.HTTPError(404, 'category not found')
|
||||
fm = self.db.field_metadata
|
||||
try:
|
||||
category_name = fm[category]['name']
|
||||
dt = fm[category]['datatype']
|
||||
except:
|
||||
if category != 'newest':
|
||||
if category not in ('newest', 'allbooks'):
|
||||
raise
|
||||
category_name = _('Newest')
|
||||
category_name = {
|
||||
'newest' : _('Newest'),
|
||||
'allbooks' : _('All books'),
|
||||
}[category]
|
||||
dt = None
|
||||
|
||||
hide_sort = 'true' if dt == 'series' else 'false'
|
||||
if category == 'search':
|
||||
which = unhexlify(cid)
|
||||
which = unhexlify(cid).decode('utf-8')
|
||||
try:
|
||||
ids = self.search_cache('search:"%s"'%which)
|
||||
except:
|
||||
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
|
||||
elif category == 'newest':
|
||||
ids = list(self.db.data.iterallids())
|
||||
ids = self.search_cache('')
|
||||
hide_sort = 'true'
|
||||
elif category == 'allbooks':
|
||||
ids = self.search_cache('')
|
||||
else:
|
||||
q = category
|
||||
if q == 'news':
|
||||
|
@ -10,7 +10,10 @@ from calibre.utils.ordered_dict import OrderedDict
|
||||
|
||||
class Cache(object):
|
||||
|
||||
def add_routes(self, c):
|
||||
def __init__(self):
|
||||
self.reset_caches()
|
||||
|
||||
def reset_caches(self):
|
||||
self._category_cache = OrderedDict()
|
||||
self._search_cache = OrderedDict()
|
||||
|
||||
|
@ -124,7 +124,7 @@ class ContentServer(object):
|
||||
if want_mobile:
|
||||
return self.mobile()
|
||||
|
||||
return self.static('index.html')
|
||||
return self.browse_toplevel()
|
||||
|
||||
def old(self, **kwargs):
|
||||
return self.static('index.html')
|
||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, sys
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
from calibre.library.server import server_config as config
|
||||
@ -38,50 +38,18 @@ def option_parser():
|
||||
' in the GUI'))
|
||||
return parser
|
||||
|
||||
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# exit first parent
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
|
||||
sys.exit(1)
|
||||
|
||||
# decouple from parent environment
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# exit from second parent
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
|
||||
sys.exit(1)
|
||||
|
||||
# Redirect standard file descriptors.
|
||||
si = file(stdin, 'r')
|
||||
so = file(stdout, 'a+')
|
||||
se = file(stderr, 'a+', 0)
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
parser = option_parser()
|
||||
opts, args = parser.parse_args(args)
|
||||
if opts.daemonize and not iswindows:
|
||||
daemonize()
|
||||
from cherrypy.process.plugins import Daemonizer
|
||||
d = Daemonizer(cherrypy.engine)
|
||||
d.subscribe()
|
||||
if opts.pidfile is not None:
|
||||
with open(opts.pidfile, 'wb') as f:
|
||||
f.write(str(os.getpid()))
|
||||
from cherrypy.process.plugins import PIDFile
|
||||
PIDFile(cherrypy.engine, opts.pidfile).subscribe()
|
||||
cherrypy.log.screen = True
|
||||
from calibre.utils.config import prefs
|
||||
if opts.with_library is None:
|
||||
|
@ -112,7 +112,6 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
||||
CLASS('thumbnail'))
|
||||
|
||||
data = TD()
|
||||
last = None
|
||||
for fmt in book['formats'].split(','):
|
||||
a = ascii_filename(book['authors'])
|
||||
t = ascii_filename(book['title'])
|
||||
@ -124,9 +123,11 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
||||
),
|
||||
CLASS('button'))
|
||||
s.tail = u''
|
||||
last = s
|
||||
data.append(s)
|
||||
|
||||
div = DIV(CLASS('data-container'))
|
||||
data.append(div)
|
||||
|
||||
series = u'[%s - %s]'%(book['series'], book['series_index']) \
|
||||
if book['series'] else ''
|
||||
tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
|
||||
@ -137,13 +138,13 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
||||
if val:
|
||||
ctext += '%s=[%s] '%tuple(val.split(':#:'))
|
||||
|
||||
text = u'\u202f%s %s by %s - %s - %s %s %s' % (book['title'], series,
|
||||
book['authors'], book['size'], book['timestamp'], tags, ctext)
|
||||
|
||||
if last is None:
|
||||
data.text = text
|
||||
else:
|
||||
last.tail += text
|
||||
first = SPAN(u'\u202f%s %s by %s' % (book['title'], series,
|
||||
book['authors']), CLASS('first-line'))
|
||||
div.append(first)
|
||||
second = SPAN(u'%s - %s %s %s' % ( book['size'],
|
||||
book['timestamp'],
|
||||
tags, ctext), CLASS('second-line'))
|
||||
div.append(second)
|
||||
|
||||
bookt.append(TR(thumbnail, data))
|
||||
# }}}
|
||||
@ -229,7 +230,7 @@ class MobileServer(object):
|
||||
no_tag_count=True)
|
||||
book['title'] = record[FM['title']]
|
||||
for x in ('timestamp', 'pubdate'):
|
||||
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
||||
book[x] = strftime('%b, %Y', record[FM[x]])
|
||||
book['id'] = record[FM['id']]
|
||||
books.append(book)
|
||||
for key in CKEYS:
|
||||
|
@ -435,3 +435,33 @@ And since I'm sure someone will ask: The reason adding/saving books are in separ
|
||||
|
||||
Finally, the reason calibre keep workers alive and idle instead of launching on demand is to workaround the slow startup time of python processes.
|
||||
|
||||
How do I run parts of |app| like news download and the content server on my own linux server?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
First, you must install |app| onto your linux server. If your server is using a modern linux distro, you should have no problems installing |app| onto it.
|
||||
|
||||
.. note::
|
||||
If you bought into the notion that a real server must run a decade old version of Debian, then you will have to jump through a few hoops. First, compile a newer version of glibc (>= 2.10) on your server from source. Then get the |app| linux binary tarball from the |app| google code page for your server architecture. Extract it into :file:`/opt/calibre`. Put your previously compiled glibc into :file:`/opt/calibre` as :file:`libc.so.6`. You can now run the calibre binaries from :file:`/opt/calibre`.
|
||||
|
||||
You can run the |app| server via the command::
|
||||
|
||||
/opt/calibre/calibre-server --with-library /path/to/the/library/you/want/to/share
|
||||
|
||||
You can download news and convert it into an ebook with the command::
|
||||
|
||||
/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub
|
||||
|
||||
If you want to generate MOBI, use outputfile.mobi instead.
|
||||
|
||||
You can email downloaded news with the command::
|
||||
|
||||
/opt/calibre/calibre-smtp
|
||||
|
||||
I leave figuring out the exact command line as an exercise for the reader.
|
||||
|
||||
Finally, you can add downloaded news to the |app| library with::
|
||||
|
||||
/opt/calibre/calibredb add --with-library /path/to/library outfile.epub
|
||||
|
||||
Remember to read the command line documentatation section of the |app| User Manual to learn more about these, and other commands.
|
||||
|
||||
|
@ -7,6 +7,7 @@ Created on 23 Sep 2010
|
||||
import re, string, traceback
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.utils.titlecase import titlecase
|
||||
|
||||
class TemplateFormatter(string.Formatter):
|
||||
'''
|
||||
@ -81,7 +82,7 @@ class TemplateFormatter(string.Formatter):
|
||||
functions = {
|
||||
'uppercase' : (0, lambda s,x: x.upper()),
|
||||
'lowercase' : (0, lambda s,x: x.lower()),
|
||||
'titlecase' : (0, lambda s,x: x.title()),
|
||||
'titlecase' : (0, lambda s,x: titlecase(x)),
|
||||
'capitalize' : (0, lambda s,x: x.capitalize()),
|
||||
'contains' : (3, _contains),
|
||||
'ifempty' : (1, _ifempty),
|
||||
|
@ -105,7 +105,7 @@ def main():
|
||||
notifier.start()
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
if result is not None:
|
||||
if result is not None and os.path.exists(os.path.dirname(resultf)):
|
||||
cPickle.dump(result, open(resultf, 'wb'), -1)
|
||||
|
||||
notifier.queue.put(None)
|
||||
|
@ -9,6 +9,7 @@ import os
|
||||
|
||||
from calibre.utils.magick import Image, DrawingWand, create_canvas
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre import fit_image
|
||||
|
||||
def normalize_format_name(fmt):
|
||||
@ -113,7 +114,9 @@ def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0,
|
||||
|
||||
def create_text_wand(font_size, font_path=None):
|
||||
if font_path is None:
|
||||
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
|
||||
font_path = tweaks['generate_cover_title_font']
|
||||
if font_path is None:
|
||||
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
|
||||
ans = DrawingWand()
|
||||
ans.font = font_path
|
||||
ans.font_size = font_size
|
||||
@ -203,8 +206,11 @@ def create_cover_page(top_lines, logo_path, width=590, height=750,
|
||||
bottom += line.bottom_margin
|
||||
bottom -= top_lines[-1].bottom_margin
|
||||
|
||||
foot_font = tweaks['generate_cover_foot_font']
|
||||
if not foot_font:
|
||||
foot_font = P('fonts/liberation/LiberationMono-Regular.ttf')
|
||||
vanity = create_text_arc(__appname__ + ' ' + __version__, 24,
|
||||
font=P('fonts/liberation/LiberationMono-Regular.ttf'))
|
||||
font=foot_font)
|
||||
lwidth, lheight = vanity.size
|
||||
left = int(max(0, (width - lwidth)/2.))
|
||||
top = height - lheight - 10
|
||||
|
@ -58,11 +58,12 @@ def publish(desc, type, port, properties=None, add_hostname=True):
|
||||
'''
|
||||
port = int(port)
|
||||
server = start_server()
|
||||
try:
|
||||
hostname = socket.gethostname().partition('.')[0]
|
||||
except:
|
||||
hostname = 'Unknown'
|
||||
|
||||
if add_hostname:
|
||||
try:
|
||||
hostname = socket.gethostname().partition('.')[0]
|
||||
except:
|
||||
hostname = 'Unknown'
|
||||
desc += ' (on %s)'%hostname
|
||||
local_ip = get_external_ip()
|
||||
type = type+'.local.'
|
||||
|
@ -842,6 +842,9 @@ class BasicNewsRecipe(Recipe):
|
||||
except NotImplementedError:
|
||||
feeds = self.parse_feeds()
|
||||
|
||||
if not feeds:
|
||||
raise ValueError('No articles found, aborting')
|
||||
|
||||
#feeds = FeedCollection(feeds)
|
||||
|
||||
self.report_progress(0, _('Trying to download cover...'))
|
||||
@ -1104,7 +1107,7 @@ class BasicNewsRecipe(Recipe):
|
||||
mi = MetaInformation(title, [__appname__])
|
||||
mi.publisher = __appname__
|
||||
mi.author_sort = __appname__
|
||||
mi.publication_type = 'periodical:'+self.publication_type
|
||||
mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title()
|
||||
mi.timestamp = nowf()
|
||||
mi.comments = self.description
|
||||
if not isinstance(mi.comments, unicode):
|
||||
|
Loading…
x
Reference in New Issue
Block a user