[Sync] Sync with trunk r6726

This commit is contained in:
Li Fanxi 2010-10-28 21:43:04 +08:00
commit f26affae90
113 changed files with 23481 additions and 17249 deletions

View File

@ -8,24 +8,25 @@
<meta http-equiv="X-UA-Compatible" content="IE=100" />
<link rel="icon" type="image/x-icon" href="http://calibre-ebook.com/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/static/browse/browse.css" />
<link type="text/css" href="/static/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/static/jquery.multiselect.css" />
<link rel="stylesheet" type="text/css" href="{prefix}/static/browse/browse.css" />
<link type="text/css" href="{prefix}/static/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="{prefix}/static/jquery.multiselect.css" />
<script type="text/javascript" src="/static/jquery.js"></script>
<script type="text/javascript" src="/static/jquery.corner.js"></script>
<script type="text/javascript" src="{prefix}/static/jquery.js"></script>
<script type="text/javascript" src="{prefix}/static/jquery.corner.js"></script>
<script type="text/javascript"
src="/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
src="{prefix}/static/jquery_ui/js/jquery-ui-1.8.5.custom.min.js"></script>
<script type="text/javascript"
src="/static/jquery.multiselect.min.js"></script>
src="{prefix}/static/jquery.multiselect.min.js"></script>
<script type="text/javascript" src="/static/browse/browse.js"></script>
<script type="text/javascript" src="{prefix}/static/browse/browse.js"></script>
<script type="text/javascript">
var sort_cookie_name = "{sort_cookie_name}";
var sort_select_label = "{sort_select_label}";
var url_prefix = "{prefix}";
$(document).ready(function() {{
init();
{script}
@ -39,16 +40,16 @@
<div id="header">
<div class="area">
<div class="bubble">
<p><a href="/browse" title="Return to top level"
<p><a href="{prefix}/browse" title="Return to top level"
>&rarr;&nbsp;home&nbsp;&larr;</a></p>
</div>
</div>
<div id="nav-container">&nbsp;
<ul id="primary-nav">
<li><a id="nav-mobile" href="/mobile" title="A version of this website suited for mobile browsers">Mobile</a></li>
<li><a id="nav-mobile" href="{prefix}/mobile" title="A version of this website suited for mobile browsers">Mobile</a></li>
<li><a id="nav-demo" href="/old" title="The old version of this webiste">Old</a></li>
<li><a id="nav-download" href="/opds" title="An OPDS feed based version of this website, used in special purpose applications">Feed</a></li>
<li><a id="nav-demo" href="{prefix}/old" title="The old version of this webiste">Old</a></li>
<li><a id="nav-download" href="{prefix}/opds" title="An OPDS feed based version of this website, used in special purpose applications">Feed</a></li>
</ul>
</div>
@ -58,7 +59,7 @@
<input type="hidden" name="cmd" value="_s-xclick"></input>
<input type="hidden" name="hosted_button_id" value="3028915"></input>
<input type="image"
src="http://calibre-ebook.com/site_media//img/button-donate.png"
src="{prefix}/static/button-donate.png"
name="submit"></input>
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif"
width="1" height="1"></img>
@ -76,7 +77,7 @@
</select>
</div>
<div id="search_box">
<form name="search_form" action="/browse/search" method="get" accept-charset="UTF-8">
<form name="search_form" action="{prefix}/browse/search" method="get" accept-charset="UTF-8">
<input value="{initial_search}" type="text" title="Search" name="query"
class="search_input" />&nbsp;
<input type="submit" value="Search" title="Search" alt="Search" />

View File

@ -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},
@ -255,7 +257,7 @@ function booklist(hide_sort) {
function show_details(a_dom) {
var book = $(a_dom).closest('div.summary');
var bd = $('#book_details_dialog');
bd.html('<span class="loading"><img src="/static/loading.gif" alt="Loading" />Loading, please wait&hellip;</span>');
bd.html('<span class="loading"><img src="'+url_prefix+'/static/loading.gif" alt="Loading" />Loading, please wait&hellip;</span>');
bd.dialog('option', 'width', $(window).width() - 100);
bd.dialog('option', 'height', $(window).height() - 100);
bd.dialog('option', 'title', book.find('.title').text());
@ -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) {

View File

@ -1,6 +1,6 @@
<div id="details_{id}" class="details">
<div class="left">
<img alt="Cover of {title}" src="/get/cover/{id}" />
<img alt="Cover of {title}" src="{prefix}/get/cover/{id}" />
</div>
<div class="right">
<div class="field formats">{formats}</div>

View File

@ -1,6 +1,6 @@
<div id="summary_{id}" class="summary">
<div class="left">
<img alt="Cover of {title}" src="/get/thumb_90_120/{id}" />
<img alt="Cover of {title}" src="{prefix}/get/thumb_90_120/{id}" />
{get_button}
</div>
<div class="right">
@ -8,7 +8,7 @@
<span class="rating_container">{stars}</span>
<span class="series">{series}</span>
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
<a href="/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
<a href="{prefix}/browse/book/{id}" title="{permalink_tt}">{permalink}</a>
</div>
<div class="title"><strong>{title}</strong></div>
<div class="authors">{authors}</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -40,7 +40,7 @@ function create_table_headers() {
function format_url(format, id, title) {
return 'get/'+format.toLowerCase() + '/'+encodeURIComponent(title) + '_' + id+'.'+format.toLowerCase();
return url_prefix + '/get/'+format.toLowerCase() + '/'+encodeURIComponent(title) + '_' + id+'.'+format.toLowerCase();
}
function render_book(book) {
@ -101,7 +101,7 @@ function render_book(book) {
}
}
title += '</span>'
title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id);
title += '<img style="display:none" alt="" src="{1}/get/cover/{0}" /></span>'.format(id, url_prefix);
title += '<div class="comments">{0}</div>'.format(comments)
// Render authors cell
var _authors = new Array();

View File

@ -3,26 +3,27 @@
<html xmlns="http://www.w3.org/1999/xhtml" version="XHTML 1.1" xml:lang="en">
<head>
<title>calibre library</title>
<link rel="stylesheet" type="text/css" href="/static/gui.css" charset="utf-8" />
<script type="text/javascript" src="/static/date.js" charset="utf-8"></script>
<script type="text/javascript" src="/static/jquery.js" charset="utf-8"></script>
<script type="text/javascript" src="/static/gui.js" charset="utf-8"></script>
<script type="text/javascript">var url_prefix='{prefix}';</script>
<link rel="stylesheet" type="text/css" href="{prefix}/static/gui.css" charset="utf-8" />
<script type="text/javascript" src="{prefix}/static/date.js" charset="utf-8"></script>
<script type="text/javascript" src="{prefix}/static/jquery.js" charset="utf-8"></script>
<script type="text/javascript" src="{prefix}/static/gui.js" charset="utf-8"></script>
<link rel="icon" href="http://calibre-ebook.com/favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="banner">
<a style="border: 0pt" href="http://calibre-ebook.com" alt="calibre" title="calibre"><img style="border:0pt" src="/static/calibre_banner.png" alt="calibre" /></a>
<a style="border: 0pt" href="http://calibre-ebook.com" alt="calibre" title="calibre"><img style="border:0pt" src="{prefix}/static/calibre_banner.png" alt="calibre" /></a>
</div>
<div id="search_box">
<form name="search_form" onsubmit="search();return false;" action="./" method="get" accept-charset="UTF-8">
<form name="search_form" onsubmit="search();return false;" action="{prefix}/old" method="get" accept-charset="UTF-8">
<input value="" id="s" type="text" />
<input type="image" src="/static/btn_search_box.png" width="27" height="24" id="go" alt="Search" title="Search" />
<input type="image" src="{prefix}/static/btn_search_box.png" width="27" height="24" id="go" alt="Search" title="Search" />
</form>
</div>
<div id="count_bar">
<span id="left"><img src="/static/first.png" alt="Show first set of books" title="Show first set of books"/>&nbsp;<img src="/static/previous.png" alt="Show previous set of books" title="Show previous set of books"/>&nbsp;</span><span id="count">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>&nbsp;<span id="right"><img src="/static/next.png" alt="Show next set of books" title="Show next set of books"/>&nbsp;<img src="/static/last.png" alt="Show last set of books" title="Show last set of books" /></span>
<span id="left"><img src="{prefix}/static/first.png" alt="Show first set of books" title="Show first set of books"/>&nbsp;<img src="{prefix}/static/previous.png" alt="Show previous set of books" title="Show previous set of books"/>&nbsp;</span><span id="count">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>&nbsp;<span id="right"><img src="{prefix}/static/next.png" alt="Show next set of books" title="Show next set of books"/>&nbsp;<img src="{prefix}/static/last.png" alt="Show last set of books" title="Show last set of books" /></span>
</div>
<div id="main">
@ -38,7 +39,7 @@
<div id="loading">
<div>
<img align="top" src="/static/loading.gif" alt="Loading..." title="Loading..."/>&nbsp;<span id="loading_msg">Loading&hellip;</span>
<img align="top" src="{prefix}/static/loading.gif" alt="Loading..." title="Loading..."/>&nbsp;<span id="loading_msg">Loading&hellip;</span>
</div>
</div>

View File

@ -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;
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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)

View File

@ -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

View File

@ -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 = [

View File

@ -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'

View 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

View File

@ -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()

View File

@ -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","&#8216;",string)
# Replace rsquo (\x92)
fixed = re.sub("\x92","&#8217;",fixed)
# Replace ldquo (\x93)
fixed = re.sub("\x93","&#8220;",fixed)
# Replace rdquo (\x94)
fixed = re.sub("\x94","&#8221;",fixed)
# Replace ndash (\x96)
fixed = re.sub("\x96","&#8211;",fixed)
# Replace mdash (\x97)
fixed = re.sub("\x97","&#8212;",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 '&#38;'
massaged = re.sub("&","&#38;", 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

View 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')
]

View 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)

View File

@ -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')]

View File

@ -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 = [

View File

@ -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

View File

@ -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:')

View File

@ -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
@ -664,7 +673,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, ]

View File

@ -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],

View File

@ -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]

View File

@ -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):

View File

@ -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())

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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'

View File

@ -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])

View File

@ -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 {{{

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View 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

View File

@ -8,88 +8,114 @@ Fetch metadata using Amazon AWS
'''
import sys, re
from lxml import etree
from lxml import html
from calibre import browser
from calibre.utils.date import parse_date, utcnow
from calibre.ebooks.metadata import MetaInformation, string_to_authors
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.chardet import xml_to_unicode
AWS_NS = 'http://webservices.amazon.com/AWSECommerceService/2005-10-05'
def find_asin(br, isbn):
q = 'http://www.amazon.com/s?field-keywords='+isbn
raw = br.open_novisit(q).read()
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
root = html.fromstring(raw)
revs = root.xpath('//*[@class="asinReviewsSummary" and @name]')
revs = [x.get('name') for x in revs]
if revs:
return revs[0]
def AWS(tag):
return '{%s}%s'%(AWS_NS, tag)
class ISBNNotFound(ValueError):
pass
def check_for_errors(root, isbn):
err = root.find('.//'+AWS('Error'))
if err is not None:
text = etree.tostring(err, method='text', pretty_print=True,
encoding=unicode)
if 'AWS.InvalidParameterValue'+isbn in text:
raise ISBNNotFound(isbn)
raise Exception('Failed to get metadata with error: '\
+ text)
def to_asin(br, isbn):
if len(isbn) == 13:
try:
asin = find_asin(br, isbn)
except:
import traceback
traceback.print_exc()
asin = None
else:
asin = isbn
return asin
def get_social_metadata(title, authors, publisher, isbn):
mi = MetaInformation(title, authors)
if isbn:
br = browser()
response_xml = br.open('http://status.calibre-ebook.com/aws/metadata/'+isbn).read()
root = etree.fromstring(response_xml)
try:
check_for_errors(root, isbn)
except ISBNNotFound:
return mi
mi.title = root.findtext('.//'+AWS('Title'))
authors = [x.text for x in root.findall('.//'+AWS('Author'))]
if authors:
mi.authors = []
for x in authors:
mi.authors.extend(string_to_authors(x))
mi.publisher = root.findtext('.//'+AWS('Publisher'))
try:
d = root.findtext('.//'+AWS('PublicationDate'))
if d:
default = utcnow().replace(day=15)
d = parse_date(d[0].text, assume_utc=True, default=default)
mi.pubdate = d
except:
pass
try:
rating = float(root.findtext('.//'+AWS('AverageRating')))
num_of_reviews = int(root.findtext('.//'+AWS('TotalReviews')))
if num_of_reviews > 4 and rating > 0 and rating < 5:
mi.rating = rating
except:
pass
tags = [x.text for x in root.findall('.//%s/%s'%(AWS('Subjects'),
AWS('Subject')))]
if tags:
mi.tags = []
for x in tags:
mi.tags.extend([y.strip() for y in x.split('/')])
mi.tags = [x.replace(',', ';') for x in mi.tags]
comments = root.find('.//%s/%s'%(AWS('EditorialReview'),
AWS('Content')))
if comments is not None:
mi.comments = etree.tostring(comments,
method='text', encoding=unicode)
mi.comments = re.sub('<([pP]|DIV)>', '\n\n', mi.comments)
mi.comments = re.sub('</?[iI]>', '*', mi.comments)
mi.comments = re.sub('</?[bB]>', '**', mi.comments)
mi.comments = re.sub('<BR>', '\n\n', mi.comments)
mi.comments = re.sub('<[^>]+>', '', mi.comments)
mi.comments = mi.comments.strip()
mi.comments = _('EDITORIAL REVIEW')+':\n\n'+mi.comments
mi = Metadata(title, authors)
if not isbn:
return mi
isbn = check_isbn(isbn)
if not isbn:
return mi
br = browser()
asin = to_asin(br, isbn)
if asin:
if get_metadata(br, asin, mi):
return mi
from calibre.ebooks.metadata.xisbn import xisbn
for i in xisbn.get_associated_isbns(isbn):
asin = to_asin(br, i)
if get_metadata(br, asin, mi):
return mi
return mi
def get_metadata(br, asin, mi):
q = 'http://amzn.com/'+asin
try:
raw = br.open_novisit(q).read()
except Exception, e:
if callable(getattr(e, 'getcode', None)) and \
e.getcode() == 404:
return False
raise
if '<title>404 - ' in raw:
return False
raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0]
root = html.fromstring(raw)
ratings = root.xpath('//form[@id="handleBuy"]/descendant::*[@class="asinReviewsSummary"]')
if ratings:
pat = re.compile(r'([0-9.]+) out of (\d+) stars')
r = ratings[0]
for elem in r.xpath('descendant::*[@title]'):
t = elem.get('title')
m = pat.match(t)
if m is not None:
try:
mi.rating = float(m.group(1))/float(m.group(2)) * 5
break
except:
pass
desc = root.xpath('//div[@id="productDescription"]/*[@class="content"]')
if desc:
desc = desc[0]
for c in desc.xpath('descendant::*[@class="seeAll" or'
' @class="emptyClear" or @href]'):
c.getparent().remove(c)
desc = html.tostring(desc, method='html', encoding=unicode).strip()
desc = re.sub(r' class=[^>]+>', '>', desc)
desc = re.sub('\n+', '\n', desc)
desc = re.sub(' +', ' ', desc)
desc = re.sub(r'(?s)<em>--This text ref.*?</em>', '', desc)
desc = re.sub(r'(?s)<!--.*?-->', '', desc)
mi.comments = desc
return True
def main(args=sys.argv):
print get_social_metadata(None, None, None, '9781416551720')
# Test xisbn
print get_social_metadata('Learning Python', None, None, '8324616489')
print
# Test sophisticated comment formatting
print get_social_metadata('Swan Thieves', None, None, '9780316065795')
print
# Random tests
print get_social_metadata('Star Trek: Destiny: Mere Mortals', None, None, '9781416551720')
print
print get_social_metadata('The Great Gatsby', None, None, '0743273567')
return 0
if __name__ == '__main__':

View File

@ -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'),

View File

@ -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)

View File

@ -0,0 +1,80 @@
#!/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'
import threading, re, json
from calibre import browser
class xISBN(object):
QUERY = 'http://xisbn.worldcat.org/webservices/xid/isbn/%s?method=getEditions&format=json&fl=form,year,lang,ed'
def __init__(self):
self.lock = threading.RLock()
self._data = []
self._map = {}
self.br = browser()
self.isbn_pat = re.compile(r'[^0-9X]', re.IGNORECASE)
def purify(self, isbn):
return self.isbn_pat.sub('', isbn.upper())
def fetch_data(self, isbn):
url = self.QUERY%isbn
data = self.br.open_novisit(url).read()
data = json.loads(data)
if data.get('stat', None) != 'ok':
return []
data = data.get('list', [])
ans = []
for rec in data:
forms = rec.get('form', [])
# Only get books, not audio/video
forms = [x for x in forms if x in ('BA', 'BC', 'BB', 'DA')]
if forms:
ans.append(rec)
return ans
def get_data(self, isbn):
isbn = self.purify(isbn)
with self.lock:
if isbn not in self._map:
try:
data = self.fetch_data(isbn)
except:
import traceback
traceback.print_exc()
data = []
id_ = len(self._data)
self._data.append(data)
for rec in data:
for i in rec.get('isbn', []):
self._map[i] = id_
self._map[isbn] = id_
return self._data[self._map[isbn]]
def get_associated_isbns(self, isbn):
data = self.get_data(isbn)
ans = set([])
for rec in data:
for i in rec.get('isbn', []):
ans.add(i)
return ans
xisbn = xISBN()
if __name__ == '__main__':
import sys
isbn = sys.argv[-1]
print xisbn.get_data(isbn)
print
print xisbn.get_associated_isbns(isbn)

View File

@ -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

View File

@ -2043,12 +2043,16 @@ class MobiWriter(object):
else :
self._oeb.logger.info("chapterCount: %d" % self._chapterCount)
if True:
rec_count = len(self._ctoc_records)
self._oeb.logger.info(" CNCX utilization: %d %s %.0f%% full" % \
(rec_count + 1, 'records, last record' if rec_count else 'record,', len(self._ctoc.getvalue())/655) )
# Apparently the CTOC must end with a null byte
self._ctoc.write('\0')
return align_block(self._ctoc.getvalue())
ctoc = self._ctoc.getvalue()
rec_count = len(self._ctoc_records)
self._oeb.logger.info(" CNCX utilization: %d %s %.0f%% full" % \
(rec_count + 1, 'records, last record' if rec_count else 'record,',
len(ctoc)/655) )
return align_block(ctoc)
def _write_periodical_node(self, indxt, indices, index, offset, length, count, firstSection, lastSection) :
pos = 0xc0 + indxt.tell()

View File

@ -25,6 +25,7 @@ from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
from calibre import isbytestring
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
@ -404,7 +405,8 @@ class DirContainer(object):
def __init__(self, path, log):
self.log = log
path = unicode(path)
if isbytestring(path):
path = path.decode(filesystem_encoding)
ext = os.path.splitext(path)[1].lower()
if ext == '.opf':
self.opfname = os.path.basename(path)

View File

@ -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()

View File

@ -255,7 +255,7 @@ def error_dialog(parent, title, msg, det_msg='', show=False,
return d
def question_dialog(parent, title, msg, det_msg='', show_copy_button=True,
buttons=QMessageBox.Yes|QMessageBox.No):
buttons=QMessageBox.Yes|QMessageBox.No, yes_button=QMessageBox.Yes):
d = MessageBox(QMessageBox.Question, title, msg, buttons,
parent, det_msg)
d.setIconPixmap(QPixmap(I('dialog_question.png')))
@ -263,7 +263,7 @@ def question_dialog(parent, title, msg, det_msg='', show_copy_button=True,
if not show_copy_button:
d.cb.setVisible(False)
return d.exec_() == QMessageBox.Yes
return d.exec_() == yes_button
def info_dialog(parent, title, msg, det_msg='', show=False):
d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok,
@ -515,7 +515,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)
@ -535,7 +535,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:

View File

@ -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)

View File

@ -111,7 +111,7 @@ class ViewAction(InterfaceAction):
'books at once can be slow and have a negative effect on the '
'responsiveness of your computer. Once started the process '
'cannot be stopped until complete. Do you wish to continue?'
) % num)
) % num, show_copy_button=False)
def view_folder(self, *args):
rows = self.gui.current_view().selectionModel().selectedRows()

View File

@ -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

View File

@ -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

View File

@ -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'), '')

View File

@ -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)

View File

@ -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)

View File

@ -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'),

View File

@ -9,11 +9,11 @@ add/remove formats
import os, re, time, traceback, textwrap
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
choose_files, choose_images, ResizableDialog, \
warning_dialog
warning_dialog, question_dialog
from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
@ -608,9 +608,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def edit_tags(self):
if self.tags.text() != self.original_tags:
error_dialog(self, _('Cannot use tag editor'),
_('The tags editor cannot be used if you have modified the tags')).exec_()
return
if question_dialog(self, _('Tags changed'),
_('You have changed the tags. In order to use the tags'
' editor, you must either discard or apply these '
'changes'), show_copy_button=False,
buttons=QMessageBox.Apply|QMessageBox.Discard,
yes_button=QMessageBox.Apply):
self.apply_tags(commit=True, notify=True)
self.original_tags = unicode(self.tags.text())
else:
self.tags.setText(self.original_tags)
d = TagEditor(self, self.db, self.row)
d.exec_()
if d.result() == QDialog.Accepted:
@ -764,6 +771,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setCurrentIndex(i)
break
def apply_tags(self, commit=False, notify=False):
self.db.set_tags(self.id, [x.strip() for x in
unicode(self.tags.text()).split(',')],
notify=notify, commit=commit)
def accept(self):
cf = getattr(self, 'cover_fetcher', None)
@ -787,11 +798,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
notify=False, commit=False)
self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
commit=False)
self.apply_tags()
self.db.set_publisher(self.id,
unicode(self.publisher.currentText()).strip(),
notify=False, commit=False)
self.db.set_tags(self.id, [x.strip() for x in
unicode(self.tags.text()).split(',')], notify=False, commit=False)
self.db.set_series(self.id,
unicode(self.series.currentText()).strip(), notify=False,
commit=False)

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
QTimer, QRect
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
from calibre.constants import isosx
from calibre.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image
from calibre.utils.fonts import fontconfig
@ -303,7 +303,8 @@ class FontFamilyModel(QAbstractListModel):
return NONE
if role == Qt.DisplayRole:
return QVariant(family)
if role == Qt.FontRole:
if not isosx and role == Qt.FontRole:
# Causes a Qt crash with some fonts on OS X
return QVariant(QFont(family))
return NONE

View File

@ -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
# }}}

View File

@ -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)))

View File

@ -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)

View File

@ -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+', ' ')
]

View File

@ -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()

View File

@ -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]

View File

@ -44,6 +44,10 @@ def server_config(defaults=None):
'by first letter when there are more than this number '
'of items. Default: %default. Set to a large number '
'to disable grouping.'))
c.add_opt('url_prefix', ['--url-prefix'], default='',
help=_('Prefix to prepend to all URLs. Useful for reverse'
'proxying to this server from Apache/nginx/etc.'))
return c
def custom_fields_to_display(db):

View File

@ -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
@ -27,16 +28,19 @@ from calibre.library.server.browse import BrowseServer
class DispatchController(object): # {{{
def __init__(self):
def __init__(self, prefix):
self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
self.funcs = []
self.seen = set([])
self.prefix = prefix if prefix else ''
def __call__(self, name, route, func, **kwargs):
if name in self.seen:
raise NameError('Route name: '+ repr(name) + ' already used')
self.seen.add(name)
kwargs['action'] = 'f_%d'%len(self.funcs)
if route != '/':
route = self.prefix + route
self.dispatcher.connect(name, route, self, **kwargs)
self.funcs.append(expose(func))
@ -54,16 +58,45 @@ class DispatchController(object): # {{{
# }}}
class BonJour(SimplePlugin): # {{{
def __init__(self, engine, port=8080, prefix=''):
SimplePlugin.__init__(self, engine)
self.port = port
self.prefix = prefix
def start(self):
try:
publish_zeroconf('Books in calibre', '_stanza._tcp',
self.port, {'path':self.prefix+'/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 +104,15 @@ 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
cherrypy.engine.bonjour.prefix = opts.url_prefix
Cache.__init__(self)
self.set_database(db)
cherrypy.config.update({
'log.screen' : opts.develop,
'engine.autoreload_on' : opts.develop,
@ -97,18 +138,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
@ -132,7 +183,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def start(self):
self.is_running = False
d = DispatchController()
d = DispatchController(self.opts.url_prefix)
for x in self.__class__.__bases__:
if hasattr(x, 'add_routes'):
x.add_routes(self, d)
@ -140,7 +191,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 +204,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)

View File

@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
import operator, os, json
from binascii import hexlify, unhexlify
from urllib import quote, unquote
import cherrypy
@ -21,7 +22,7 @@ from calibre.library.comments import comments_to_html
from calibre.library.server import custom_fields_to_display
from calibre.library.field_metadata import category_icon_map
def render_book_list(ids, suffix=''): # {{{
def render_book_list(ids, prefix, suffix=''): # {{{
pages = []
num = len(ids)
pos = 0
@ -34,11 +35,11 @@ def render_book_list(ids, suffix=''): # {{{
page_template = u'''\
<div class="page" id="page{0}">
<div class="load_data" title="{1}">
<span class="url" title="/browse/booklist_page"></span>
<span class="url" title="{prefix}/browse/booklist_page"></span>
<span class="start" title="{start}"></span>
<span class="end" title="{end}"></span>
</div>
<div class="loading"><img src="/static/loading.gif" /> {2}</div>
<div class="loading"><img src="{prefix}/static/loading.gif" /> {2}</div>
<div class="loaded"></div>
</div>
'''
@ -48,7 +49,7 @@ def render_book_list(ids, suffix=''): # {{{
ld = xml(json.dumps(pg), True)
rpages.append(page_template.format(i, ld,
xml(_('Loading, please wait')) + '&hellip;',
start=pos+1, end=pos+len(pg)))
start=pos+1, end=pos+len(pg), prefix=prefix))
rpages = u'\n\n'.join(rpages)
templ = u'''\
@ -90,7 +91,7 @@ def utf8(x): # {{{
return x
# }}}
def render_rating(rating, container='span', prefix=None): # {{{
def render_rating(rating, url_prefix, container='span', prefix=None): # {{{
if rating < 0.1:
return '', ''
added = 0
@ -107,22 +108,25 @@ def render_rating(rating, container='span', prefix=None): # {{{
elif n >= 0.9:
x = 'on'
ans.append(
u'<img alt="{0}" title="{0}" src="/static/star-{1}.png" />'.format(
rstring, x))
u'<img alt="{0}" title="{0}" src="{2}/static/star-{1}.png" />'.format(
rstring, x, url_prefix))
added += 1
ans.append('</%s>'%container)
return u''.join(ans), rstring
# }}}
def get_category_items(category, items, db, datatype): # {{{
def get_category_items(category, items, restriction, datatype, prefix): # {{{
if category == 'search':
items = [x for x in items if x.name != restriction]
def item(i):
templ = (u'<div title="{4}" class="category-item">'
'<div class="category-name">{0}</div><div>{1}</div>'
'<div>{2}'
'<span class="href">{3}</span></div></div>')
rating, rstring = render_rating(i.avg_rating)
'<span class="href">{5}{3}</span></div></div>')
rating, rstring = render_rating(i.avg_rating, prefix)
name = xml(i.name)
if datatype == 'rating':
name = xml(_('%d stars')%int(i.avg_rating))
@ -136,9 +140,9 @@ 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)
xml(desc), xml(href), rstring, prefix)
items = list(map(item, items))
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
@ -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()
@ -236,6 +243,7 @@ class BrowseServer(object):
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
ans = ans.replace('{sort_cookie_name}', scn)
ans = ans.replace('{prefix}', self.opts.url_prefix)
opts = ['<option %svalue="%s">%s</option>' % (
'selected="selected" ' if k==sort else '',
xml(k), xml(n), ) for k, n in
@ -251,15 +259,14 @@ class BrowseServer(object):
ans = ans.replace('{initial_search}', initial_search)
return ans
return self.__browse_template__
@property
def browse_summary_template(self):
if not hasattr(self, '__browse_summary_template__') or \
self.opts.develop:
self.__browse_summary_template__ = \
P('content_server/browse/summary.html', data=True).decode('utf-8')
return self.__browse_summary_template__
return self.__browse_summary_template__.replace('{prefix}',
self.opts.url_prefix)
@property
def browse_details_template(self):
@ -267,7 +274,8 @@ class BrowseServer(object):
self.opts.develop:
self.__browse_details_template__ = \
P('content_server/browse/details.html', data=True).decode('utf-8')
return self.__browse_details_template__
return self.__browse_details_template__.replace('{prefix}',
self.opts.url_prefix)
# }}}
@ -298,6 +306,7 @@ class BrowseServer(object):
category_meta = self.db.field_metadata
cats = [
(_('Newest'), 'newest', 'forward.png'),
(_('All books'), 'allbooks', 'book.png'),
]
def getter(x):
@ -326,11 +335,11 @@ class BrowseServer(object):
icon = 'blank.png'
cats.append((meta['name'], category, icon))
cats = [('<li title="{2} {0}"><img src="{src}" alt="{0}" />'
cats = [('<li title="{2} {0}"><img src="{3}{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')),
src='/browse/icon/'+z)
'<span class="url">{3}/browse/category/{1}</span></li>')
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
self.opts.url_prefix, src='/browse/icon/'+z)
for x, y, z in cats]
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
@ -369,7 +378,9 @@ 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,
self.opts.url_prefix)
else:
getter = lambda x: unicode(getattr(x, 'sort', x.name))
starts = set([])
@ -384,12 +395,13 @@ class BrowseServer(object):
getter(y).upper().startswith(x)])
items = [(u'<h3 title="{0}">{0} <span>[{2}]</span></h3><div>'
u'<div class="loaded" style="display:none"></div>'
u'<div class="loading"><img alt="{1}" src="/static/loading.gif" /><em>{1}</em></div>'
u'<span class="load_href">{3}</span></div>').format(
u'<div class="loading"><img alt="{1}" src="{4}/static/loading.gif" /><em>{1}</em></div>'
u'<span class="load_href">{4}{3}</span></div>').format(
xml(s, True),
xml(_('Loading, please wait'))+'&hellip;',
unicode(c),
xml(u'/browse/category_group/%s/%s'%(category, s)))
xml(u'/browse/category_group/%s/%s'%(category, s)),
self.opts.url_prefix)
for s, c in category_groups.items()]
items = '\n\n'.join(items)
items = u'<div id="groups">\n{0}</div>'.format(items)
@ -401,13 +413,13 @@ class BrowseServer(object):
main = u'''
<div class="category">
<h3>{0}</h3>
<a class="navlink" href="/browse"
<a class="navlink" href="{3}/browse"
title="{2}">{2}&nbsp;&uarr;</a>
{1}
</div>
'''.format(
xml(_('Browsing by')+': ' + category_name), items,
xml(_('Up'), True))
xml(_('Up'), True), self.opts.url_prefix)
return self.browse_template(sort).format(title=category_name,
script=script, main=main)
@ -439,7 +451,9 @@ 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,
self.opts.url_prefix)
return json.dumps(entries, ensure_ascii=False)
@ -449,7 +463,11 @@ class BrowseServer(object):
if category == None:
ans = self.browse_toplevel()
elif category == 'newest':
raise cherrypy.InternalRedirect('/browse/matches/newest/dummy')
raise cherrypy.InternalRedirect(self.opts.url_prefix +
'/browse/matches/newest/dummy')
elif category == 'allbooks':
raise cherrypy.InternalRedirect(self.opts.url_prefix +
'/browse/matches/allbooks/dummy')
else:
ans = self.browse_category(category, category_sort)
@ -473,32 +491,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':
@ -512,7 +538,8 @@ class BrowseServer(object):
list_sort = category
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in') + ' ' + category_name)
html = render_book_list(ids, self.opts.url_prefix,
suffix=_('in') + ' ' + category_name)
return self.browse_template(sort, category=False).format(
title=_('Books in') + " " +category_name,
@ -560,17 +587,18 @@ class BrowseServer(object):
if fmts and fmt:
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
if other_fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(f, fname, id_, f.upper()) for f in
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(f, fname, id_, f.upper(),
self.opts.url_prefix) for f in
other_fmts]
ofmts = ', '.join(ofmts)
args['other_formats'] = u'<strong>%s: </strong>' % \
_('Other formats') + ofmts
args['details_href'] = '/browse/details/'+str(id_)
args['details_href'] = self.opts.url_prefix + '/browse/details/'+str(id_)
if fmt:
href = '/get/%s/%s_%d.%s'%(
href = self.opts.url_prefix + '/get/%s/%s_%d.%s'%(
fmt, fname, id_, fmt)
rt = xml(_('Read %s in the %s format')%(args['title'],
fmt.upper()), True)
@ -583,7 +611,8 @@ class BrowseServer(object):
args['comments'] = comments_to_html(mi.comments)
args['stars'] = ''
if mi.rating:
args['stars'] = render_rating(mi.rating/2.0, prefix=_('Rating'))[0]
args['stars'] = render_rating(mi.rating/2.0,
self.opts.url_prefix, prefix=_('Rating'))[0]
if args['tags']:
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
args['tags']
@ -608,8 +637,9 @@ class BrowseServer(object):
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
args['formats'] = ''
if fmts:
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(fmt, fname, id_, fmt.upper()) for fmt in
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
.format(fmt, fname, id_, fmt.upper(),
self.opts.url_prefix) for fmt in
fmts]
ofmts = ', '.join(ofmts)
args['formats'] = ofmts
@ -628,7 +658,8 @@ class BrowseServer(object):
continue
if m['datatype'] == 'rating':
r = u'<strong>%s: </strong>'%xml(m['name']) + \
render_rating(mi.rating/2.0, prefix=m['name'])[0]
render_rating(mi.rating/2.0, self.opts.url_prefix,
prefix=m['name'])[0]
else:
r = u'<strong>%s: </strong>'%xml(m['name']) + \
args[field]
@ -684,7 +715,8 @@ class BrowseServer(object):
items = [self.db.data._data[x] for x in ids]
sort = self.browse_sort_book_list(items, list_sort)
ids = [x[0] for x in items]
html = render_book_list(ids, suffix=_('in search')+': '+query)
html = render_book_list(ids, self.opts.url_prefix,
suffix=_('in search')+': '+query)
return self.browse_template(sort, category=False, initial_search=query).format(
title=_('Matching books'),
script='booklist();', main=html)

View File

@ -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()

View File

@ -103,7 +103,11 @@ class ContentServer(object):
if self.opts.develop:
lm = fromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(lm)
return open(path, 'rb').read()
with open(path, 'rb') as f:
ans = f.read()
if path.endswith('.css'):
ans = ans.replace('/static/', self.opts.url_prefix + '/static/')
return ans
def index(self, **kwargs):
'The / URL'
@ -124,10 +128,11 @@ 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')
return self.static('index.html').replace('{prefix}',
self.opts.url_prefix)
# Actually get content from the database {{{
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80):

View File

@ -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
@ -25,7 +25,18 @@ def stop_threaded_server(server):
server.thread = None
def option_parser():
parser = config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.'))
parser = config().option_parser('%prog '+ _(
'''[options]
Start the calibre content server. The calibre content server
exposes your calibre library over the internet. The default interface
allows you to browse you calibre library by categories. You can also
access an interface optimized for mobile browsers at /mobile and an
OPDS based interface for use with reading applications at /opds.
The OPDS interface is advertised via BonJour automatically.
'''
))
parser.add_option('--with-library', default=None,
help=_('Path to the library folder to serve with the content server'))
parser.add_option('--pidfile', default=None,
@ -38,50 +49,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:

View File

@ -26,9 +26,9 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python
return kwargs
def build_search_box(num, search, sort, order): # {{{
def build_search_box(num, search, sort, order, prefix): # {{{
div = DIV(id='search_box')
form = FORM('Show ', method='get', action='mobile')
form = FORM('Show ', method='get', action=prefix+'/mobile')
form.set('accept-charset', 'UTF-8')
div.append(form)
@ -89,11 +89,12 @@ def build_navigation(start, num, total, url_base): # {{{
# }}}
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
prefix):
logo = DIV(IMG(src=prefix+'/static/calibre.png', alt=__appname__), id='logo')
search_box = build_search_box(num, search, sort, order)
navigation = build_navigation(start, num, total, url_base)
search_box = build_search_box(num, search, sort, order, prefix)
navigation = build_navigation(start, num, total, prefix+url_base)
bookt = TABLE(id='listing')
body = BODY(
@ -107,26 +108,28 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
# Book list {{{
for book in books:
thumbnail = TD(
IMG(type='image/jpeg', border='0', src='/get/thumb/%s' %
IMG(type='image/jpeg', border='0',
src=prefix+'/get/thumb/%s' %
book['id']),
CLASS('thumbnail'))
data = TD()
last = None
for fmt in book['formats'].split(','):
a = ascii_filename(book['authors'])
t = ascii_filename(book['title'])
s = SPAN(
A(
fmt.lower(),
href='/get/%s/%s-%s_%d.%s' % (fmt, a, t,
href=prefix+'/get/%s/%s-%s_%d.%s' % (fmt, a, t,
book['id'], fmt)
),
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 +140,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))
# }}}
@ -153,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
TITLE(__appname__ + ' Library'),
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
type='image/x-icon'),
LINK(rel='stylesheet', type='text/css', href='/mobile/style.css')
LINK(rel='stylesheet', type='text/css', href=prefix+'/mobile/style.css')
), # End head
body
) # End html
@ -173,7 +176,9 @@ class MobileServer(object):
cherrypy.response.headers['Content-Type'] = 'text/css; charset=utf-8'
updated = utcfromtimestamp(os.stat(path).st_mtime)
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
return open(path, 'rb').read()
with open(path, 'rb') as f:
ans = f.read()
return ans.replace('{prefix}', self.opts.url_prefix)
def mobile(self, start='1', num='25', sort='date', search='',
_=None, order='descending'):
@ -229,7 +234,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:
@ -258,7 +263,8 @@ class MobileServer(object):
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
return html.tostring(build_index(books, num, search, sort, order,
start, len(ids), url_base, CKEYS),
start, len(ids), url_base, CKEYS,
self.opts.url_prefix),
encoding='utf-8', include_meta_content_type=True,
pretty_print=True)

View File

@ -132,7 +132,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
link
)
def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS):
def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix):
FM = db.FIELD_MAP
title = item[FM['title']]
if not title:
@ -185,16 +185,16 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS):
for fmt in formats.split(','):
fmt = fmt.lower()
mt = guess_type('a.'+fmt)[0]
href = '/get/%s/%s'%(fmt, item[FM['id']])
href = prefix + '/get/%s/%s'%(fmt, item[FM['id']])
if mt:
link = E.link(type=mt, href=href)
if version > 0:
link.set('rel', "http://opds-spec.org/acquisition")
ans.append(link)
ans.append(E.link(type='image/jpeg', href='/get/cover/%s'%item[FM['id']],
ans.append(E.link(type='image/jpeg', href=prefix+'/get/cover/%s'%item[FM['id']],
rel="x-stanza-cover-image" if version == 0 else
"http://opds-spec.org/cover"))
ans.append(E.link(type='image/jpeg', href='/get/thumb/%s'%item[FM['id']],
ans.append(E.link(type='image/jpeg', href=prefix+'/get/thumb/%s'%item[FM['id']],
rel="x-stanza-cover-image-thumbnail" if version == 0 else
"http://opds-spec.org/thumbnail"))
@ -275,7 +275,7 @@ class NavFeed(Feed):
class AcquisitionFeed(NavFeed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
db):
db, prefix):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
CFM = db.field_metadata
CKEYS = [key for key in sorted(custom_fields_to_display(db),
@ -283,7 +283,7 @@ class AcquisitionFeed(NavFeed):
CFM[y]['name'].lower()))]
for item in items:
self.root.append(ACQUISITION_ENTRY(item, version, db, updated,
CFM, CKEYS))
CFM, CKEYS, prefix))
class CategoryFeed(NavFeed):
@ -360,7 +360,8 @@ class OPDSServer(object):
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
return str(AcquisitionFeed(updated, id_, items, offsets,
page_url, up_url, version, self.db))
page_url, up_url, version, self.db,
self.opts.url_prefix))
def opds_search(self, query=None, version=0, offset=0):
try:

View File

@ -435,3 +435,35 @@ 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 documentation section of the |app| User Manual to learn more about these, and other commands.
.. note:: Some parts of calibre require a X server. If you're lucky, nothing you do will fall into this category, if not, you will have to look into using xvfb.

View File

@ -7,7 +7,7 @@ being closed.
"""
import tempfile, os, atexit, binascii, cPickle
from calibre import __version__, __appname__
from calibre.constants import __version__, __appname__
def cleanup(path):
try:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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