mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
36ec248d97
131
Changelog.yaml
131
Changelog.yaml
@ -4,6 +4,137 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.25
|
||||||
|
date: 2010-10-29
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Add support for the SONY periodical format."
|
||||||
|
description: "This means that news downloaded by calibre and sent to a newer SONY device (350/650/900) should appear in the Periodicals section and have the special periodicals navigation user interface"
|
||||||
|
type: major
|
||||||
|
|
||||||
|
- title: "Content server: Make the new browsing interface the default. The old interface can be accessed at /old"
|
||||||
|
|
||||||
|
- title: "Content server: Allow running of content server as a WSGI application within another server. Add tutorial for this to the User Manual."
|
||||||
|
|
||||||
|
- title: "Support for the Pico Life reader, Kobo Wifi and HTC Aria"
|
||||||
|
|
||||||
|
- title: "Content server: Add a new --url-prefix command line option to ease the use of the server with a reverse proxy"
|
||||||
|
|
||||||
|
- title: "New social metadata plugin for Amazon that does not rely on AWS. Since Amazon broke AWS, it is recommended you upgrade to this version if you use metadata from Amazon"
|
||||||
|
|
||||||
|
- title: "Add a tweak to specify the fonts used when geenrating the default cover"
|
||||||
|
|
||||||
|
- title: "Add an output profile for generic Tablet devices"
|
||||||
|
tickets: [7289]
|
||||||
|
|
||||||
|
- title: "SONY driver: Allow sorting of collections by arbitrary field via a new tweak."
|
||||||
|
|
||||||
|
- title: "Content server: Make /mobile a little prettier"
|
||||||
|
|
||||||
|
- title: "Add button to 'Library Check' to automatically delete spurious files and folders"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "FB2 Input: Lots of love. Handle stylesheets and style attributes. Make parsinf malformed FB2 files more robust."
|
||||||
|
tickets: [7219, 7230]
|
||||||
|
|
||||||
|
- title: "Fix auto send of news to device with multiple calibre libraries. The fix means that if you have any pending news to be sent, it will be ignored after the update. Future news downloads will once again be automatically sent to the device."
|
||||||
|
|
||||||
|
- title: "MOBI Output: Conversion of super/sub scripts now handles nested tags."
|
||||||
|
tickets: [7264]
|
||||||
|
|
||||||
|
- title: "Conversion pipeline: Fix parsing of XML encoding declarations."
|
||||||
|
tickets: [7328]
|
||||||
|
|
||||||
|
- title: "Pandigital (Kobo): Upload thumbnails to correct location"
|
||||||
|
tickets: [7165]
|
||||||
|
|
||||||
|
- title: "Fix auto emailed news with non asci characters in title not being deliverd to Kindle"
|
||||||
|
tickets: [7322]
|
||||||
|
|
||||||
|
- title: "Read metadata only after on import plugins have run when adding books to GUI"
|
||||||
|
tickets: [7245]
|
||||||
|
|
||||||
|
- title: "Various fixes for bugs caused by non ascii temporary paths on windows with non UTF-8 filesystem encodings"
|
||||||
|
tickets: [7288]
|
||||||
|
|
||||||
|
- title: "Various fixes/enhancements to SNB Output"
|
||||||
|
|
||||||
|
- title: "Allow Tag editor in edit metadata dialog to be used even if tags have been changed"
|
||||||
|
tickets: [7298]
|
||||||
|
|
||||||
|
- title: "Fix crash on some OS X machines when Preferences->Conversion->Output is clicked"
|
||||||
|
|
||||||
|
- title: "MOBI indexing: Fix last entry missing sometimes"
|
||||||
|
tickets: [6595]
|
||||||
|
|
||||||
|
- title: "Fix regression causing books to be deselected after sending to device"
|
||||||
|
tickets: [7271]
|
||||||
|
|
||||||
|
- title: "Conversion pipeline: Fix rescaling of GIF images not working"
|
||||||
|
tickets: [7306]
|
||||||
|
|
||||||
|
- title: "Update PDF metadata/conversion libraries in windows build"
|
||||||
|
|
||||||
|
- title: "Fix timezone bug when searching on date fields"
|
||||||
|
tickets: [7300]
|
||||||
|
|
||||||
|
- title: "Fix regression that caused the viewr to crash if the main application is closed"
|
||||||
|
tickets: [7276]
|
||||||
|
|
||||||
|
- title: "Fix bug causing a spurious metadata.opf file to be written at the root of the calibre library when adding books"
|
||||||
|
|
||||||
|
- title: "Use the same title casing algorithm in all places"
|
||||||
|
|
||||||
|
- title: "Fix bulk edit of dual state boolean custom columns"
|
||||||
|
|
||||||
|
- title: "Increase image size for comics in Kindle DX profile for better conversion of comics to PDF"
|
||||||
|
|
||||||
|
- title: "Fix restore db to not dies when conflicting custom columns are encountered and report conflicting columns errors. Fix exceptions when referencing invalid _index fields."
|
||||||
|
|
||||||
|
- title: "Fix auto merge books not respecting article sort tweak"
|
||||||
|
tickets: [7147]
|
||||||
|
|
||||||
|
- title: "Linux device drivers: Fix udisks based ejecting for devices with multiple nodes"
|
||||||
|
|
||||||
|
- title: "Linux device mounting: Mount the drive with the lowest kernel name as main memory"
|
||||||
|
|
||||||
|
- title: "Fix use of numeric fields in templates"
|
||||||
|
|
||||||
|
- title: "EPUB Input: Handle EPUB files with multiple OPF files."
|
||||||
|
tickets: [7229]
|
||||||
|
|
||||||
|
- title: "Setting EPUB metadata: Fix date format. Fix language being overwritten by und when unspecified. Fix empty ISBN identifier being created"
|
||||||
|
|
||||||
|
- title: "Fix cannot delete a Series listing from List view also dismiss fetch metadata dialog when no metadata found automatically"
|
||||||
|
tickets: [7221, 7220]
|
||||||
|
|
||||||
|
- title: "Content server: Handle switch library in GUI gracefully"
|
||||||
|
|
||||||
|
- title: "calibre-server: Use cherrypy implementation of --pidfile and --daemonize"
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "Ming Pao"
|
||||||
|
author: "Eddie Lau"
|
||||||
|
|
||||||
|
- title: "lenta.ru"
|
||||||
|
author: "Nikolai Kotchetkov"
|
||||||
|
|
||||||
|
- title: "frazpc.pl"
|
||||||
|
author: "Tomasz Dlugosz"
|
||||||
|
|
||||||
|
- title: "Perfil and The Economic Collapse Blog"
|
||||||
|
author: "Darko Miletic"
|
||||||
|
|
||||||
|
- title: "STNN"
|
||||||
|
author: "Larry Chan"
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- CubaDebate
|
||||||
|
- El Pais
|
||||||
|
- Fox News
|
||||||
|
- New Scientist
|
||||||
|
- The Economic Times of India
|
||||||
|
|
||||||
- version: 0.7.24
|
- version: 0.7.24
|
||||||
date: 2010-10-17
|
date: 2010-10-17
|
||||||
|
|
||||||
|
@ -8,24 +8,25 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=100" />
|
<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="icon" type="image/x-icon" href="http://calibre-ebook.com/favicon.ico" />
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/browse/browse.css" />
|
<link rel="stylesheet" type="text/css" href="{prefix}/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 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="/static/jquery.multiselect.css" />
|
<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="{prefix}/static/jquery.js"></script>
|
||||||
<script type="text/javascript" src="/static/jquery.corner.js"></script>
|
<script type="text/javascript" src="{prefix}/static/jquery.corner.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript"
|
<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"
|
<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">
|
<script type="text/javascript">
|
||||||
var sort_cookie_name = "{sort_cookie_name}";
|
var sort_cookie_name = "{sort_cookie_name}";
|
||||||
var sort_select_label = "{sort_select_label}";
|
var sort_select_label = "{sort_select_label}";
|
||||||
|
var url_prefix = "{prefix}";
|
||||||
$(document).ready(function() {{
|
$(document).ready(function() {{
|
||||||
init();
|
init();
|
||||||
{script}
|
{script}
|
||||||
@ -39,16 +40,16 @@
|
|||||||
<div id="header">
|
<div id="header">
|
||||||
<div class="area">
|
<div class="area">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<p><a href="/browse" title="Return to top level"
|
<p><a href="{prefix}/browse" title="Return to top level"
|
||||||
>→ home ←</a></p>
|
>→ home ←</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="nav-container">
|
<div id="nav-container">
|
||||||
<ul id="primary-nav">
|
<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-demo" href="{prefix}/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-download" href="{prefix}/opds" title="An OPDS feed based version of this website, used in special purpose applications">Feed</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -58,7 +59,7 @@
|
|||||||
<input type="hidden" name="cmd" value="_s-xclick"></input>
|
<input type="hidden" name="cmd" value="_s-xclick"></input>
|
||||||
<input type="hidden" name="hosted_button_id" value="3028915"></input>
|
<input type="hidden" name="hosted_button_id" value="3028915"></input>
|
||||||
<input type="image"
|
<input type="image"
|
||||||
src="http://calibre-ebook.com/site_media//img/button-donate.png"
|
src="{prefix}/static/button-donate.png"
|
||||||
name="submit"></input>
|
name="submit"></input>
|
||||||
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif"
|
<img alt="" src="https://www.paypal.com/en_US/i/scr/pixel.gif"
|
||||||
width="1" height="1"></img>
|
width="1" height="1"></img>
|
||||||
@ -76,7 +77,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="search_box">
|
<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"
|
<input value="{initial_search}" type="text" title="Search" name="query"
|
||||||
class="search_input" />
|
class="search_input" />
|
||||||
<input type="submit" value="Search" title="Search" alt="Search" />
|
<input type="submit" value="Search" title="Search" alt="Search" />
|
||||||
|
@ -257,7 +257,7 @@ function booklist(hide_sort) {
|
|||||||
function show_details(a_dom) {
|
function show_details(a_dom) {
|
||||||
var book = $(a_dom).closest('div.summary');
|
var book = $(a_dom).closest('div.summary');
|
||||||
var bd = $('#book_details_dialog');
|
var bd = $('#book_details_dialog');
|
||||||
bd.html('<span class="loading"><img src="/static/loading.gif" alt="Loading" />Loading, please wait…</span>');
|
bd.html('<span class="loading"><img src="'+url_prefix+'/static/loading.gif" alt="Loading" />Loading, please wait…</span>');
|
||||||
bd.dialog('option', 'width', $(window).width() - 100);
|
bd.dialog('option', 'width', $(window).width() - 100);
|
||||||
bd.dialog('option', 'height', $(window).height() - 100);
|
bd.dialog('option', 'height', $(window).height() - 100);
|
||||||
bd.dialog('option', 'title', book.find('.title').text());
|
bd.dialog('option', 'title', book.find('.title').text());
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div id="details_{id}" class="details">
|
<div id="details_{id}" class="details">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<img alt="Cover of {title}" src="/get/cover/{id}" />
|
<img alt="Cover of {title}" src="{prefix}/get/cover/{id}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="field formats">{formats}</div>
|
<div class="field formats">{formats}</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div id="summary_{id}" class="summary">
|
<div id="summary_{id}" class="summary">
|
||||||
<div class="left">
|
<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}
|
{get_button}
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<span class="rating_container">{stars}</span>
|
<span class="rating_container">{stars}</span>
|
||||||
<span class="series">{series}</span>
|
<span class="series">{series}</span>
|
||||||
<a href="#" onclick="show_details(this); return false;" title="{details_tt}">{details}</a>
|
<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>
|
||||||
<div class="title"><strong>{title}</strong></div>
|
<div class="title"><strong>{title}</strong></div>
|
||||||
<div class="authors">{authors}</div>
|
<div class="authors">{authors}</div>
|
||||||
|
BIN
resources/content_server/button-donate.png
Normal file
BIN
resources/content_server/button-donate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -40,7 +40,7 @@ function create_table_headers() {
|
|||||||
|
|
||||||
|
|
||||||
function format_url(format, id, title) {
|
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) {
|
function render_book(book) {
|
||||||
@ -101,7 +101,7 @@ function render_book(book) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
title += '</span>'
|
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)
|
title += '<div class="comments">{0}</div>'.format(comments)
|
||||||
// Render authors cell
|
// Render authors cell
|
||||||
var _authors = new Array();
|
var _authors = new Array();
|
||||||
|
@ -3,26 +3,27 @@
|
|||||||
<html xmlns="http://www.w3.org/1999/xhtml" version="XHTML 1.1" xml:lang="en">
|
<html xmlns="http://www.w3.org/1999/xhtml" version="XHTML 1.1" xml:lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>calibre library</title>
|
<title>calibre library</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/gui.css" charset="utf-8" />
|
<script type="text/javascript">var url_prefix='{prefix}';</script>
|
||||||
<script type="text/javascript" src="/static/date.js" charset="utf-8"></script>
|
<link rel="stylesheet" type="text/css" href="{prefix}/static/gui.css" charset="utf-8" />
|
||||||
<script type="text/javascript" src="/static/jquery.js" charset="utf-8"></script>
|
<script type="text/javascript" src="{prefix}/static/date.js" charset="utf-8"></script>
|
||||||
<script type="text/javascript" src="/static/gui.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" />
|
<link rel="icon" href="http://calibre-ebook.com/favicon.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="banner">
|
<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>
|
||||||
|
|
||||||
<div id="search_box">
|
<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 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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="count_bar">
|
<div id="count_bar">
|
||||||
<span id="left"><img src="/static/first.png" alt="Show first set of books" title="Show first set of books"/> <img src="/static/previous.png" alt="Show previous set of books" title="Show previous set of books"/> </span><span id="count"> </span> <span id="right"><img src="/static/next.png" alt="Show next set of books" title="Show next set of books"/> <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"/> <img src="{prefix}/static/previous.png" alt="Show previous set of books" title="Show previous set of books"/> </span><span id="count"> </span> <span id="right"><img src="{prefix}/static/next.png" alt="Show next set of books" title="Show next set of books"/> <img src="{prefix}/static/last.png" alt="Show last set of books" title="Show last set of books" /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
@ -38,7 +39,7 @@
|
|||||||
|
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
<div>
|
<div>
|
||||||
<img align="top" src="/static/loading.gif" alt="Loading..." title="Loading..."/> <span id="loading_msg">Loading…</span>
|
<img align="top" src="{prefix}/static/loading.gif" alt="Loading..." title="Loading..."/> <span id="loading_msg">Loading…</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -203,3 +203,11 @@ content_server_wont_display = ['']
|
|||||||
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
|
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
|
||||||
maximum_resort_levels = 5
|
maximum_resort_levels = 5
|
||||||
|
|
||||||
|
# Absolute path to a TTF font file to use as the font for the title and author
|
||||||
|
# when generating a default cover. Useful if the default font (Liberation
|
||||||
|
# Serif) does not contain glyphs for the language of the books in your library.
|
||||||
|
generate_cover_title_font = None
|
||||||
|
|
||||||
|
# Absolute path to a TTF font file to use as the font for the footer in the
|
||||||
|
# default cover
|
||||||
|
generate_cover_foot_font = None
|
||||||
|
BIN
resources/images/news/perfil.png
Normal file
BIN
resources/images/news/perfil.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 781 B |
@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__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
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
@ -13,32 +11,44 @@ class CubaDebate(BasicNewsRecipe):
|
|||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Contra el Terorismo Mediatico'
|
description = 'Contra el Terorismo Mediatico'
|
||||||
oldest_article = 15
|
oldest_article = 15
|
||||||
language = 'es'
|
language = 'es'
|
||||||
|
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
publisher = 'Cubadebate'
|
publisher = 'Cubadebate'
|
||||||
category = 'news, politics, Cuba'
|
category = 'news, politics, Cuba'
|
||||||
encoding = 'utf-8'
|
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 = {
|
conversion_options = {
|
||||||
'comments' : description
|
'comments' : description
|
||||||
,'tags' : category
|
,'tags' : category
|
||||||
,'language' : 'es'
|
,'language' : language
|
||||||
,'publisher' : publisher
|
,'publisher' : publisher
|
||||||
,'pretty_print': True
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'Outline'})]
|
keep_only_tags = [dict(name='div', attrs={'id':'Outline'})]
|
||||||
remove_tags_after = dict(name='div',attrs={'id':'BlogContent'})
|
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/')]
|
feeds = [(u'Articulos', u'http://www.cubadebate.cu/feed/')]
|
||||||
|
remove_attributes=['width','height','lang']
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + 'print/'
|
return url + 'print/'
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
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
|
||||||
|
177
resources/recipes/lenta_ru.recipe
Normal file
177
resources/recipes/lenta_ru.recipe
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
'''
|
||||||
|
Lenta.ru
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.feedparser import parse
|
||||||
|
from calibre.ebooks.BeautifulSoup import Tag
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
import re
|
||||||
|
|
||||||
|
class LentaRURecipe(BasicNewsRecipe):
|
||||||
|
title = u'Lenta.ru: \u041d\u043e\u0432\u043e\u0441\u0442\u0438'
|
||||||
|
__author__ = 'Nikolai Kotchetkov'
|
||||||
|
publisher = 'lenta.ru'
|
||||||
|
category = 'news, Russia'
|
||||||
|
description = u'''\u0415\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u0430\u044f
|
||||||
|
\u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442-\u0433\u0430\u0437\u0435\u0442\u0430.
|
||||||
|
\u041d\u043e\u0432\u043e\u0441\u0442\u0438 \u0441\u043e
|
||||||
|
\u0432\u0441\u0435\u0433\u043e \u043c\u0438\u0440\u0430 \u043d\u0430
|
||||||
|
\u0440\u0443\u0441\u0441\u043a\u043e\u043c
|
||||||
|
\u044f\u0437\u044b\u043a\u0435'''
|
||||||
|
description = u'Ежедневная интернет-газета. Новости со всего мира на русском языке'
|
||||||
|
oldest_article = 3
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
masthead_url = u'http://img.lenta.ru/i/logowrambler.gif'
|
||||||
|
cover_url = u'http://img.lenta.ru/i/logowrambler.gif'
|
||||||
|
|
||||||
|
#Add feed names if you want them to be sorted (feeds of this list appear first)
|
||||||
|
sortOrder = [u'_default', u'В России', u'б.СССР', u'В мире']
|
||||||
|
|
||||||
|
encoding = 'cp1251'
|
||||||
|
language = 'ru'
|
||||||
|
no_stylesheets = True
|
||||||
|
remove_javascript = True
|
||||||
|
recursions = 0
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='td', attrs={'class':['statya','content']})]
|
||||||
|
|
||||||
|
remove_tags_after = [dict(name='p', attrs={'class':'links'}), dict(name='div', attrs={'id':'readers-block'})]
|
||||||
|
|
||||||
|
remove_tags = [dict(name='table', attrs={'class':['vrezka','content']}), dict(name='div', attrs={'class':'b240'}), dict(name='div', attrs={'id':'readers-block'}), dict(name='p', attrs={'class':'links'})]
|
||||||
|
|
||||||
|
feeds = [u'http://lenta.ru/rss/']
|
||||||
|
|
||||||
|
extra_css = 'h1 {font-size: 1.2em; margin: 0em 0em 0em 0em;} h2 {font-size: 1.0em; margin: 0em 0em 0em 0em;} h3 {font-size: 0.8em; margin: 0em 0em 0em 0em;}'
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
try:
|
||||||
|
feedData = parse(self.feeds[0])
|
||||||
|
if not feedData:
|
||||||
|
raise NotImplementedError
|
||||||
|
self.log("parse_index: Feed loaded successfully.")
|
||||||
|
if feedData.feed.has_key('title'):
|
||||||
|
self.title = feedData.feed.title
|
||||||
|
self.log("parse_index: Title updated to: ", self.title)
|
||||||
|
if feedData.feed.has_key('image'):
|
||||||
|
self.log("HAS IMAGE!!!!")
|
||||||
|
|
||||||
|
def get_virtual_feed_articles(feed):
|
||||||
|
if feeds.has_key(feed):
|
||||||
|
return feeds[feed][1]
|
||||||
|
self.log("Adding new feed: ", feed)
|
||||||
|
articles = []
|
||||||
|
feeds[feed] = (feed, articles)
|
||||||
|
return articles
|
||||||
|
|
||||||
|
feeds = {}
|
||||||
|
|
||||||
|
#Iterate feed items and distribute articles using tags
|
||||||
|
for item in feedData.entries:
|
||||||
|
link = item.get('link', '');
|
||||||
|
title = item.get('title', '');
|
||||||
|
if '' == link or '' == title:
|
||||||
|
continue
|
||||||
|
article = {'title':title, 'url':link, 'description':item.get('description', ''), 'date':item.get('date', ''), 'content':''};
|
||||||
|
if not item.has_key('tags'):
|
||||||
|
get_virtual_feed_articles('_default').append(article)
|
||||||
|
continue
|
||||||
|
for tag in item.tags:
|
||||||
|
addedToDefault = False
|
||||||
|
term = tag.get('term', '')
|
||||||
|
if '' == term:
|
||||||
|
if (not addedToDefault):
|
||||||
|
get_virtual_feed_articles('_default').append(article)
|
||||||
|
continue
|
||||||
|
get_virtual_feed_articles(term).append(article)
|
||||||
|
|
||||||
|
#Get feed list
|
||||||
|
#Select sorted feeds first of all
|
||||||
|
result = []
|
||||||
|
for feedName in self.sortOrder:
|
||||||
|
if (not feeds.has_key(feedName)): continue
|
||||||
|
result.append(feeds[feedName])
|
||||||
|
del feeds[feedName]
|
||||||
|
result = result + feeds.values()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
self.log(err)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
||||||
|
def postprocess_html(self, soup, first_fetch):
|
||||||
|
#self.log('Original: ', soup.prettify())
|
||||||
|
|
||||||
|
contents = Tag(soup, 'div')
|
||||||
|
|
||||||
|
#Extract tags with given attributes
|
||||||
|
extractElements = {'div' : [{'id' : 'readers-block'}]}
|
||||||
|
|
||||||
|
#Remove all elements that were not extracted before
|
||||||
|
for tag, attrs in extractElements.iteritems():
|
||||||
|
for attr in attrs:
|
||||||
|
garbage = soup.findAll(tag, attr)
|
||||||
|
if garbage:
|
||||||
|
for pieceOfGarbage in garbage:
|
||||||
|
pieceOfGarbage.extract()
|
||||||
|
|
||||||
|
#Find article text using header
|
||||||
|
#and add all elements to contents
|
||||||
|
element = soup.find({'h1' : True, 'h2' : True})
|
||||||
|
if (element):
|
||||||
|
element.name = 'h1'
|
||||||
|
while element:
|
||||||
|
nextElement = element.nextSibling
|
||||||
|
element.extract()
|
||||||
|
contents.insert(len(contents.contents), element)
|
||||||
|
element = nextElement
|
||||||
|
|
||||||
|
#Place article date after header
|
||||||
|
dates = soup.findAll(text=re.compile('\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}:\d{2}'))
|
||||||
|
if dates:
|
||||||
|
for date in dates:
|
||||||
|
for string in date:
|
||||||
|
parent = date.parent
|
||||||
|
if (parent and isinstance(parent, Tag) and 'div' == parent.name and 'dt' == parent['class']):
|
||||||
|
#Date div found
|
||||||
|
parent.extract()
|
||||||
|
parent['style'] = 'font-size: 0.5em; color: gray; font-family: monospace;'
|
||||||
|
contents.insert(1, parent)
|
||||||
|
break
|
||||||
|
|
||||||
|
#Place article picture after date
|
||||||
|
pic = soup.find('img')
|
||||||
|
if pic:
|
||||||
|
picDiv = Tag(soup, 'div')
|
||||||
|
picDiv['style'] = 'width: 100%; text-align: center;'
|
||||||
|
pic.extract()
|
||||||
|
picDiv.insert(0, pic)
|
||||||
|
title = pic.get('title', None)
|
||||||
|
if title:
|
||||||
|
titleDiv = Tag(soup, 'div')
|
||||||
|
titleDiv['style'] = 'font-size: 0.5em;'
|
||||||
|
titleDiv.insert(0, title)
|
||||||
|
picDiv.insert(1, titleDiv)
|
||||||
|
contents.insert(2, picDiv)
|
||||||
|
|
||||||
|
body = soup.find('td', {'class':['statya','content']})
|
||||||
|
if body:
|
||||||
|
body.replaceWith(contents)
|
||||||
|
|
||||||
|
#self.log('Result: ', soup.prettify())
|
||||||
|
return soup
|
||||||
|
|
@ -4,149 +4,79 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
'''
|
'''
|
||||||
nytimes.com
|
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 import strftime
|
||||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
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):
|
class NYTimes(BasicNewsRecipe):
|
||||||
|
|
||||||
title = 'The New York Times'
|
title = u'New York Times'
|
||||||
__author__ = 'GRiker'
|
__author__ = 'Kovid Goyal/Nick Redding'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
requires_version = (0, 7, 5)
|
requires_version = (0, 6, 36)
|
||||||
|
|
||||||
description = 'Daily news from the New York Times (subscription version)'
|
description = 'Daily news from the New York Times (subscription version)'
|
||||||
allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials',
|
timefmt = ' [%b %d]'
|
||||||
'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 = ''
|
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
remove_tags_before = dict(id='article')
|
remove_tags_before = dict(id='article')
|
||||||
remove_tags_after = dict(id='article')
|
remove_tags_after = dict(id='article')
|
||||||
remove_tags = [dict(attrs={'class':[
|
remove_tags = [dict(attrs={'class':['articleTools', 'post-tools', 'side_tool','nextArticleLink',
|
||||||
'articleFooter',
|
'nextArticleLink clearfix','columnGroup doubleRule','doubleRule','entry-meta',
|
||||||
'articleTools',
|
'icon enlargeThis','columnGroup last','relatedSearchesModule']}),
|
||||||
'columnGroup doubleRule',
|
dict({'class':re.compile('^subNavigation')}),
|
||||||
'columnGroup singleRule',
|
dict({'class':re.compile('^leaderboard')}),
|
||||||
'columnGroup last',
|
dict({'class':re.compile('^module')}),
|
||||||
'columnGroup last',
|
dict({'class':'metaFootnote'}),
|
||||||
'doubleRule',
|
dict(id=['inlineBox','footer', 'toolsRight', 'articleInline','login','masthead',
|
||||||
'dottedLine',
|
'navigation', 'archive', 'side_search', 'blog_sidebar','cCol','portfolioInline',
|
||||||
'entry-meta',
|
'side_tool', 'side_index','header','readerReviewsCount','readerReviews',
|
||||||
'entry-response module',
|
'relatedArticles', 'relatedTopics', 'adxSponLink']),
|
||||||
'icon enlargeThis',
|
dict(name=['script', 'noscript', 'style','form','hr'])]
|
||||||
'leftNavTabs',
|
encoding = decode
|
||||||
'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')
|
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
extra_css = '.headline {text-align: left;}\n \
|
extra_css = '''
|
||||||
.byline {font-family: monospace; \
|
.articleHeadline { margin-top:0.5em; margin-bottom:0.25em; }
|
||||||
text-align: left; \
|
.credit { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||||
margin-top: 0px; \
|
.byline { font-size: small; font-style:italic; line-height:1em; margin-top:10px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||||
margin-bottom: 0px;}\n \
|
.dateline { font-size: small; line-height:1em;margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||||
.dateline {font-size: small; \
|
.kicker { font-size: small; line-height:1em;margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||||
margin-top: 0px; \
|
.timestamp { font-size: small; }
|
||||||
margin-bottom: 0px;}\n \
|
.caption { font-size: small; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||||
.timestamp {font-size: small; \
|
a:link {text-decoration: none; }'''
|
||||||
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 '
|
|
||||||
|
|
||||||
def get_browser(self):
|
def get_browser(self):
|
||||||
br = BasicNewsRecipe.get_browser()
|
br = BasicNewsRecipe.get_browser()
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
try:
|
br.open('http://www.nytimes.com/auth/login')
|
||||||
br.open('http://www.nytimes.com/auth/login')
|
br.select_form(name='login')
|
||||||
br.select_form(name='login')
|
br['USERID'] = self.username
|
||||||
br['USERID'] = self.username
|
br['PASSWORD'] = self.password
|
||||||
br['PASSWORD'] = self.password
|
raw = br.submit().read()
|
||||||
raw = br.submit().read()
|
if 'Sorry, we could not find the combination you entered. Please try again.' in raw:
|
||||||
if 'Sorry, we could not find the combination you entered. Please try again.' in raw:
|
raise Exception('Your username and password are incorrect')
|
||||||
raise Exception('Your username and password are incorrect')
|
#open('/t/log.html', 'wb').write(raw)
|
||||||
#open('/t/log.html', 'wb').write(raw)
|
|
||||||
except:
|
|
||||||
self.log("\nFailed to login")
|
|
||||||
|
|
||||||
return br
|
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):
|
def get_cover_url(self):
|
||||||
cover = None
|
cover = None
|
||||||
st = time.localtime()
|
st = time.localtime()
|
||||||
@ -162,316 +92,101 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
cover = None
|
cover = None
|
||||||
return cover
|
return cover
|
||||||
|
|
||||||
def get_masthead_title(self):
|
def short_title(self):
|
||||||
return self.title
|
return 'New York Times'
|
||||||
|
|
||||||
def dump_ans(self, ans):
|
|
||||||
total_article_count = 0
|
|
||||||
for section in ans :
|
|
||||||
if self.verbose:
|
|
||||||
self.log("section %s: %d articles" % (section[0], len(section[1])) )
|
|
||||||
for article in section[1]:
|
|
||||||
total_article_count += 1
|
|
||||||
if self.verbose:
|
|
||||||
self.log("\t%-40.40s... \t%-60.60s..." % (article['title'].encode('mac-roman','replace'),
|
|
||||||
article['url'].encode('mac-roman','replace')))
|
|
||||||
self.log( "Queued %d articles" % total_article_count )
|
|
||||||
|
|
||||||
def dump_hex(self, src, length=16):
|
|
||||||
''' Diagnostic '''
|
|
||||||
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
|
|
||||||
N=0; result=''
|
|
||||||
while src:
|
|
||||||
s,src = src[:length],src[length:]
|
|
||||||
hexa = ' '.join(["%02X"%ord(x) for x in s])
|
|
||||||
s = s.translate(FILTER)
|
|
||||||
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
|
|
||||||
N+=length
|
|
||||||
print result
|
|
||||||
|
|
||||||
def fixChars(self,string):
|
|
||||||
# Replace lsquo (\x91)
|
|
||||||
fixed = re.sub("\x91","‘",string)
|
|
||||||
|
|
||||||
# Replace rsquo (\x92)
|
|
||||||
fixed = re.sub("\x92","’",fixed)
|
|
||||||
|
|
||||||
# Replace ldquo (\x93)
|
|
||||||
fixed = re.sub("\x93","“",fixed)
|
|
||||||
|
|
||||||
# Replace rdquo (\x94)
|
|
||||||
fixed = re.sub("\x94","”",fixed)
|
|
||||||
|
|
||||||
# Replace ndash (\x96)
|
|
||||||
fixed = re.sub("\x96","–",fixed)
|
|
||||||
|
|
||||||
# Replace mdash (\x97)
|
|
||||||
fixed = re.sub("\x97","—",fixed)
|
|
||||||
|
|
||||||
return fixed
|
|
||||||
|
|
||||||
def massageNCXText(self, description):
|
|
||||||
# Kindle TOC descriptions won't render certain characters
|
|
||||||
if description:
|
|
||||||
massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
|
|
||||||
# Replace '&' with '&'
|
|
||||||
massaged = re.sub("&","&", massaged)
|
|
||||||
return self.fixChars(massaged)
|
|
||||||
else:
|
|
||||||
return description
|
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
|
self.encoding = 'cp1252'
|
||||||
soup = self.index_to_soup('http://www.nytimes.com/pages/todayspaper/index.html')
|
soup = self.index_to_soup('http://www.nytimes.com/pages/todayspaper/index.html')
|
||||||
|
self.encoding = decode
|
||||||
|
|
||||||
def feed_title(div):
|
def feed_title(div):
|
||||||
return ''.join(div.findAll(text=True, recursive=False)).strip()
|
return ''.join(div.findAll(text=True, recursive=True)).strip()
|
||||||
|
|
||||||
articles = {}
|
articles = {}
|
||||||
key = None
|
key = None
|
||||||
ans = []
|
ans = []
|
||||||
# Find each instance of class="section-headline", class="story", class="story headline"
|
url_list = []
|
||||||
for div in soup.findAll(True,
|
|
||||||
attrs={'class':['section-headline', 'story', 'story headline']}):
|
|
||||||
|
|
||||||
if div['class'] == 'section-headline':
|
def handle_article(div):
|
||||||
key = string.capwords(feed_title(div))
|
a = div.find('a', href=True)
|
||||||
if self.excludeSectionKeywords:
|
if not a:
|
||||||
excluded = re.compile('|'.join(self.excludeSectionKeywords))
|
return
|
||||||
if excluded.search(key):
|
url = re.sub(r'\?.*', '', a['href'])
|
||||||
self.log("Skipping section %s" % key)
|
if not url.startswith("http"):
|
||||||
continue
|
return
|
||||||
articles[key] = []
|
if not url.endswith(".html"):
|
||||||
ans.append(key)
|
return
|
||||||
|
if 'podcast' in url:
|
||||||
elif div['class'] in ['story', 'story headline'] :
|
return
|
||||||
a = div.find('a', href=True)
|
url += '?pagewanted=all'
|
||||||
if not a:
|
if url in url_list:
|
||||||
continue
|
return
|
||||||
url = re.sub(r'\?.*', '', a['href'])
|
url_list.append(url)
|
||||||
url += '?pagewanted=all'
|
title = self.tag_to_string(a, use_alt=True).strip()
|
||||||
|
#self.log("Title: %s" % title)
|
||||||
title = self.massageNCXText(self.tag_to_string(a, use_alt=True).strip())
|
description = ''
|
||||||
|
pubdate = strftime('%a, %d %b')
|
||||||
description = ''
|
summary = div.find(True, attrs={'class':'summary'})
|
||||||
pubdate = strftime('%a, %d %b')
|
if summary:
|
||||||
summary = div.find(True, attrs={'class':'summary'})
|
description = self.tag_to_string(summary, use_alt=False)
|
||||||
if summary:
|
author = ''
|
||||||
description = self.massageNCXText(self.tag_to_string(summary, use_alt=False))
|
authorAttribution = div.find(True, attrs={'class':'byline'})
|
||||||
|
if authorAttribution:
|
||||||
author = ''
|
author = self.tag_to_string(authorAttribution, use_alt=False)
|
||||||
authorAttribution = div.find(True, attrs={'class':'storyheadline-author'})
|
else:
|
||||||
|
authorAttribution = div.find(True, attrs={'class':'byline'})
|
||||||
if authorAttribution:
|
if authorAttribution:
|
||||||
author = self.tag_to_string(authorAttribution, use_alt=False)
|
author = self.tag_to_string(authorAttribution, use_alt=False)
|
||||||
else:
|
feed = key if key is not None else 'Uncategorized'
|
||||||
authorAttribution = div.find(True, attrs={'class':'byline'})
|
if not articles.has_key(feed):
|
||||||
if authorAttribution:
|
articles[feed] = []
|
||||||
author = self.tag_to_string(authorAttribution, use_alt=False)
|
articles[feed].append(
|
||||||
# Kill commas - Kindle switches to '&'
|
dict(title=title, url=url, date=pubdate,
|
||||||
author = re.sub(',','',author)
|
description=description, author=author,
|
||||||
|
content=''))
|
||||||
|
|
||||||
feed = key if key is not None else 'Uncategorized'
|
|
||||||
if not articles.has_key(feed):
|
|
||||||
articles[feed] = []
|
# Find each instance of class="section-headline", class="story", class="story headline"
|
||||||
if not 'podcasts' in url:
|
for div in soup.findAll(True,
|
||||||
articles[feed].append(
|
attrs={'class':['section-headline', 'story', 'story headline','sectionHeader','headlinesOnly multiline flush']}):
|
||||||
dict(title=title, url=url, date=pubdate,
|
|
||||||
description=description, author=author,
|
if div['class'] in ['section-headline','sectionHeader']:
|
||||||
content=''))
|
key = string.capwords(feed_title(div))
|
||||||
ans = self.sort_index_by(ans, {'The Front Page':-1,
|
articles[key] = []
|
||||||
'Dining In, Dining Out':1,
|
ans.append(key)
|
||||||
'Obituaries':2})
|
#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)]
|
ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
|
||||||
self.dump_ans(ans)
|
|
||||||
return 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):
|
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
|
|
||||||
|
|
||||||
|
66
resources/recipes/perfil.recipe
Normal file
66
resources/recipes/perfil.recipe
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
perfil.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Perfil(BasicNewsRecipe):
|
||||||
|
title = 'Perfil'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Noticias de Argentina y el resto del mundo'
|
||||||
|
publisher = 'perfil.com'
|
||||||
|
category = 'news, politics, Argentina'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'cp1252'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'es'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
masthead_url = 'http://www.perfil.com/export/sites/diarioperfil/arte/10/logo_perfilcom_mm.gif'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Arial,Helvetica,sans-serif }
|
||||||
|
.seccion{border-bottom: 1px dotted #666666; text-transform: uppercase; font-size: x-large}
|
||||||
|
.foto1 h1{font-size: x-small}
|
||||||
|
h1{font-family: Georgia,"Times New Roman",serif}
|
||||||
|
img{margin-bottom: 0.4em}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name=['iframe','embed','object','base','meta','link'])
|
||||||
|
,dict(name='a', attrs={'href':'#comentarios'})
|
||||||
|
,dict(name='div', attrs={'class':'foto3'})
|
||||||
|
,dict(name='img', attrs={'alt':'ampliar'})
|
||||||
|
]
|
||||||
|
keep_only_tags=[dict(attrs={'class':['bd468a','cuerpoSuperior']})]
|
||||||
|
remove_attributes=['onload','lang','width','height','border']
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Ultimo momento' , u'http://www.perfil.com/rss/ultimomomento.xml')
|
||||||
|
,(u'Politica' , u'http://www.perfil.com/rss/politica.xml' )
|
||||||
|
,(u'Policia' , u'http://www.perfil.com/rss/policia.xml' )
|
||||||
|
,(u'Internacionales', u'http://www.perfil.com/rss/internacional.xml')
|
||||||
|
,(u'Economia' , u'http://www.perfil.com/rss/economia.xml' )
|
||||||
|
,(u'Deportes' , u'http://www.perfil.com/rss/deportes.xml' )
|
||||||
|
,(u'Opinion' , u'http://www.perfil.com/rss/columnistas.xml' )
|
||||||
|
,(u'Sociedad' , u'http://www.perfil.com/rss/sociedad.xml' )
|
||||||
|
,(u'Cultura' , u'http://www.perfil.com/rss/cultura.xml' )
|
||||||
|
,(u'Espectaculos' , u'http://www.perfil.com/rss/espectaculos.xml' )
|
||||||
|
,(u'Ciencia' , u'http://www.perfil.com/rss/ciencia.xml' )
|
||||||
|
,(u'Salud' , u'http://www.perfil.com/rss/salud.xml' )
|
||||||
|
,(u'Tecnologia' , u'http://www.perfil.com/rss/tecnologia.xml' )
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
@ -30,23 +30,40 @@
|
|||||||
<title>
|
<title>
|
||||||
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
|
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
|
||||||
</title>
|
</title>
|
||||||
<style type="text/x-oeb1-css">
|
<style type="text/css">
|
||||||
A { color : #0002CC }
|
a { color : #0002CC }
|
||||||
A:HOVER { color : #BF0000 }
|
|
||||||
BODY { background-color : #FEFEFE; color : #000000; font-family : Verdana, Geneva, Arial, Helvetica, sans-serif; text-align : justify }
|
a:hover { color : #BF0000 }
|
||||||
H1{ font-size : 160%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Black; background-color : #E7E7E7; margin-left : 0px; page-break-before : always; }
|
|
||||||
H2{ font-size : 130%; font-style : normal; font-weight : bold; text-align : left; background-color : #EEEEEE; border : 1px solid Gray; page-break-before : always; }
|
body { background-color : #FEFEFE; color : #000000; font-family : Verdana, Geneva, Arial, Helvetica, sans-serif; text-align : justify }
|
||||||
H3{ font-size : 110%; font-style : normal; font-weight : bold; text-align : left; background-color : #F1F1F1; border : 1px solid Silver;}
|
|
||||||
H4{ font-size : 100%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
h1{ font-size : 160%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Black; background-color : #E7E7E7; margin-left : 0px; page-break-before : always; }
|
||||||
H5{ font-size : 100%; font-style : italic; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
|
||||||
H6{ font-size : 100%; font-style : italic; font-weight : normal; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
h2{ font-size : 130%; font-style : normal; font-weight : bold; text-align : left; background-color : #EEEEEE; border : 1px solid Gray; page-break-before : always; }
|
||||||
SMALL{ font-size : 80% }
|
|
||||||
BLOCKQUOTE{ margin-left :4em; margin-top:1em; margin-right:0.2em;}
|
h3{ font-size : 110%; font-style : normal; font-weight : bold; text-align : left; background-color : #F1F1F1; border : 1px solid Silver;}
|
||||||
HR{ color : Black }
|
|
||||||
DIV{font-family : "Times New Roman", Times, serif; text-align : justify}
|
h4{ font-size : 100%; font-style : normal; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
||||||
UL{margin-left: 0}
|
|
||||||
.epigraph{width:50%; margin-left : 35%;}
|
h5{ font-size : 100%; font-style : italic; font-weight : bold; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
||||||
|
|
||||||
|
h6{ font-size : 100%; font-style : italic; font-weight : normal; text-align : left; border : 1px solid Gray; background-color : #F4F4F4;}
|
||||||
|
|
||||||
|
small { font-size : 80% }
|
||||||
|
|
||||||
|
blockquote { margin-left :4em; margin-top:1em; margin-right:0.2em;}
|
||||||
|
|
||||||
|
hr { color : Black }
|
||||||
|
|
||||||
|
div {font-family : "Times New Roman", Times, serif; text-align : justify}
|
||||||
|
|
||||||
|
ul {margin-left: 0}
|
||||||
|
|
||||||
|
.epigraph{width:50%; margin-left : 35%;}
|
||||||
|
|
||||||
|
div.paragraph { text-align: justify; text-indent: 2em; }
|
||||||
</style>
|
</style>
|
||||||
|
<link rel="stylesheet" type="text/css" href="inline-styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<xsl:for-each select="fb:description/fb:title-info/fb:annotation">
|
<xsl:for-each select="fb:description/fb:title-info/fb:annotation">
|
||||||
@ -136,12 +153,13 @@
|
|||||||
</xsl:choose>
|
</xsl:choose>
|
||||||
</xsl:variable>
|
</xsl:variable>
|
||||||
<xsl:if test="$section_has_title = 'None'">
|
<xsl:if test="$section_has_title = 'None'">
|
||||||
<a name="TOC_{generate-id()}" />
|
<div id="TOC_{generate-id()}">
|
||||||
<xsl:if test="@id">
|
<xsl:if test="@id">
|
||||||
<xsl:element name="a">
|
<xsl:element name="a">
|
||||||
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
|
||||||
</xsl:element>
|
</xsl:element>
|
||||||
</xsl:if>
|
</xsl:if>
|
||||||
|
</div>
|
||||||
</xsl:if>
|
</xsl:if>
|
||||||
<xsl:apply-templates>
|
<xsl:apply-templates>
|
||||||
<xsl:with-param name="section_toc_id" select="$section_has_title" />
|
<xsl:with-param name="section_toc_id" select="$section_has_title" />
|
||||||
@ -160,13 +178,13 @@
|
|||||||
</xsl:if>
|
</xsl:if>
|
||||||
<xsl:if test="$section_toc_id != 'None'">
|
<xsl:if test="$section_toc_id != 'None'">
|
||||||
<xsl:element name="a">
|
<xsl:element name="a">
|
||||||
<xsl:attribute name="name">TOC_<xsl:value-of select="$section_toc_id"/></xsl:attribute>
|
<xsl:attribute name="id">TOC_<xsl:value-of select="$section_toc_id"/></xsl:attribute>
|
||||||
</xsl:element>
|
</xsl:element>
|
||||||
</xsl:if>
|
</xsl:if>
|
||||||
<a name="TOC_{generate-id()}"></a>
|
<a name="TOC_{generate-id()}"></a>
|
||||||
<xsl:if test="@id">
|
<xsl:if test="@id">
|
||||||
<xsl:element name="a">
|
<xsl:element name="a">
|
||||||
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
|
||||||
</xsl:element>
|
</xsl:element>
|
||||||
</xsl:if>
|
</xsl:if>
|
||||||
<xsl:apply-templates/>
|
<xsl:apply-templates/>
|
||||||
@ -176,7 +194,7 @@
|
|||||||
<xsl:element name="h6">
|
<xsl:element name="h6">
|
||||||
<xsl:if test="@id">
|
<xsl:if test="@id">
|
||||||
<xsl:element name="a">
|
<xsl:element name="a">
|
||||||
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
|
||||||
</xsl:element>
|
</xsl:element>
|
||||||
</xsl:if>
|
</xsl:if>
|
||||||
<xsl:apply-templates/>
|
<xsl:apply-templates/>
|
||||||
@ -207,11 +225,18 @@
|
|||||||
</xsl:template>
|
</xsl:template>
|
||||||
<!-- p -->
|
<!-- p -->
|
||||||
<xsl:template match="fb:p">
|
<xsl:template match="fb:p">
|
||||||
<div align="justify"><xsl:if test="@id">
|
<xsl:element name="div">
|
||||||
|
<xsl:attribute name="class">paragraph</xsl:attribute>
|
||||||
|
<xsl:if test="@id">
|
||||||
<xsl:element name="a">
|
<xsl:element name="a">
|
||||||
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
||||||
</xsl:element>
|
</xsl:element>
|
||||||
</xsl:if>    <xsl:apply-templates/></div>
|
</xsl:if>
|
||||||
|
<xsl:if test="@style">
|
||||||
|
<xsl:attribute name="style"><xsl:value-of select="@style"/></xsl:attribute>
|
||||||
|
</xsl:if>
|
||||||
|
<xsl:apply-templates/>
|
||||||
|
</xsl:element>
|
||||||
</xsl:template>
|
</xsl:template>
|
||||||
<!-- strong -->
|
<!-- strong -->
|
||||||
<xsl:template match="fb:strong">
|
<xsl:template match="fb:strong">
|
||||||
|
@ -20,20 +20,4 @@ function setup_image_scaling_handlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function extract_svged_images() {
|
|
||||||
$("svg").each(function() {
|
|
||||||
var children = $(this).children("img");
|
|
||||||
if (children.length == 1) {
|
|
||||||
var img = $(children[0]);
|
|
||||||
var href = img.attr('xlink:href');
|
|
||||||
if (href != undefined) {
|
|
||||||
$(this).replaceWith('<div style="text-align:center; margin: 0; padding: 0"><img style="height: 98%" alt="SVG Image" src="' + href +'"></img></div>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
//extract_svged_images();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ reflow_error = poppler_error if poppler_error else magick_error
|
|||||||
|
|
||||||
pdfreflow_libs = []
|
pdfreflow_libs = []
|
||||||
if iswindows:
|
if iswindows:
|
||||||
pdfreflow_libs = ['advapi32', 'User32', 'Gdi32']
|
pdfreflow_libs = ['advapi32', 'User32', 'Gdi32', 'zlib']
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ It contains correct fonts.conf etc.
|
|||||||
poppler
|
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
|
NOTE: poppler must be built as a static library, unless you build the qt4 bindings
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.24'
|
__version__ = '0.7.25'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -292,7 +292,7 @@ class RTFMetadataReader(MetadataReaderPlugin):
|
|||||||
def get_metadata(self, stream, ftype):
|
def get_metadata(self, stream, ftype):
|
||||||
from calibre.ebooks.metadata.rtf import get_metadata
|
from calibre.ebooks.metadata.rtf import get_metadata
|
||||||
return get_metadata(stream)
|
return get_metadata(stream)
|
||||||
|
|
||||||
class SNBMetadataReader(MetadataReaderPlugin):
|
class SNBMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
name = 'Read SNB metadata'
|
name = 'Read SNB metadata'
|
||||||
@ -471,7 +471,8 @@ from calibre.devices.iriver.driver import IRIVER_STORY
|
|||||||
from calibre.devices.binatone.driver import README
|
from calibre.devices.binatone.driver import README
|
||||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||||
from calibre.devices.edge.driver import EDGE
|
from calibre.devices.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, SOVOS
|
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
|
||||||
|
SOVOS, PICO
|
||||||
from calibre.devices.sne.driver import SNE
|
from calibre.devices.sne.driver import SNE
|
||||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
||||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO
|
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO
|
||||||
@ -572,6 +573,7 @@ plugins += [
|
|||||||
ELONEX,
|
ELONEX,
|
||||||
TECLAST_K3,
|
TECLAST_K3,
|
||||||
NEWSMY,
|
NEWSMY,
|
||||||
|
PICO,
|
||||||
IPAPYRUS,
|
IPAPYRUS,
|
||||||
SOVOS,
|
SOVOS,
|
||||||
EDGE,
|
EDGE,
|
||||||
|
@ -4,6 +4,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import sys
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
from xml.sax.saxutils import escape
|
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):
|
class SonyReaderOutput(OutputProfile):
|
||||||
|
|
||||||
@ -650,13 +658,14 @@ class NookOutput(OutputProfile):
|
|||||||
|
|
||||||
class BambookOutput(OutputProfile):
|
class BambookOutput(OutputProfile):
|
||||||
|
|
||||||
|
author = 'Li Fanxi'
|
||||||
name = 'Sanda Bambook'
|
name = 'Sanda Bambook'
|
||||||
short_name = 'bambook'
|
short_name = 'bambook'
|
||||||
description = _('This profile is intended for the Sanda Bambook.')
|
description = _('This profile is intended for the Sanda Bambook.')
|
||||||
|
|
||||||
# Screen size is a best guess
|
# Screen size is a best guess
|
||||||
screen_size = (800, 600)
|
screen_size = (600, 800)
|
||||||
comic_screen_size = (700, 540)
|
comic_screen_size = (540, 700)
|
||||||
dpi = 168.451
|
dpi = 168.451
|
||||||
fbase = 12
|
fbase = 12
|
||||||
fsizes = [10, 12, 14, 16]
|
fsizes = [10, 12, 14, 16]
|
||||||
@ -664,7 +673,7 @@ class BambookOutput(OutputProfile):
|
|||||||
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
||||||
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
||||||
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
|
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
|
||||||
iPadOutput, KoboReaderOutput,
|
iPadOutput, KoboReaderOutput, TabletOutput,
|
||||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||||
BambookOutput, ]
|
BambookOutput, ]
|
||||||
|
@ -20,7 +20,8 @@ class ANDROID(USBMS):
|
|||||||
VENDOR_ID = {
|
VENDOR_ID = {
|
||||||
# HTC
|
# HTC
|
||||||
0x0bb4 : { 0x0c02 : [0x100, 0x0227], 0x0c01 : [0x100, 0x0227], 0x0ff9
|
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
|
# Motorola
|
||||||
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
|
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
|
||||||
|
@ -117,6 +117,12 @@ class PDNOVEL_KOBO(PDNOVEL):
|
|||||||
|
|
||||||
EBOOK_DIR_MAIN = 'eBooks/Kobo'
|
EBOOK_DIR_MAIN = 'eBooks/Kobo'
|
||||||
|
|
||||||
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
|
coverdata = getattr(metadata, 'thumbnail', None)
|
||||||
|
if coverdata and coverdata[2]:
|
||||||
|
with open(os.path.join(path, '.thumbnail', filename+'.jpg'), 'wb') as coverfile:
|
||||||
|
coverfile.write(coverdata[2])
|
||||||
|
|
||||||
|
|
||||||
class VELOCITYMICRO(USBMS):
|
class VELOCITYMICRO(USBMS):
|
||||||
name = 'VelocityMicro device interface'
|
name = 'VelocityMicro device interface'
|
||||||
|
@ -36,15 +36,15 @@ class N770(USBMS):
|
|||||||
|
|
||||||
class N810(N770):
|
class N810(N770):
|
||||||
name = 'Nokia 810 Device Interface'
|
name = 'Nokia 810 Device Interface'
|
||||||
gui_name = 'Nokia 810'
|
gui_name = 'Nokia 810/900'
|
||||||
description = _('Communicate with the Nokia 810 internet tablet.')
|
description = _('Communicate with the Nokia 810/900 internet tablet.')
|
||||||
|
|
||||||
PRODUCT_ID = [0x96]
|
PRODUCT_ID = [0x96, 0x1c7]
|
||||||
BCD = [0x316]
|
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):
|
class E71X(USBMS):
|
||||||
|
|
||||||
|
@ -573,7 +573,10 @@ class XMLCache(object):
|
|||||||
ans = root.makeelement('{%s}text'%namespace, attrib=attrib,
|
ans = root.makeelement('{%s}text'%namespace, attrib=attrib,
|
||||||
nsmap=root.nsmap)
|
nsmap=root.nsmap)
|
||||||
ans.tail = '\n'
|
ans.tail = '\n'
|
||||||
root[-1].tail = '\n' + '\t'
|
if len(root) > 0:
|
||||||
|
root[-1].tail = '\n\t'
|
||||||
|
else:
|
||||||
|
root.text = '\n\t'
|
||||||
root.append(ans)
|
root.append(ans)
|
||||||
if thumbnail and thumbnail[-1]:
|
if thumbnail and thumbnail[-1]:
|
||||||
ans.text = '\n' + '\t\t'
|
ans.text = '\n' + '\t\t'
|
||||||
|
@ -41,6 +41,15 @@ class NEWSMY(TECLAST_K3):
|
|||||||
WINDOWS_MAIN_MEM = 'NEWSMY'
|
WINDOWS_MAIN_MEM = 'NEWSMY'
|
||||||
WINDOWS_CARD_A_MEM = 'USBDISK____SD'
|
WINDOWS_CARD_A_MEM = 'USBDISK____SD'
|
||||||
|
|
||||||
|
class PICO(NEWSMY):
|
||||||
|
name = 'Pico device interface'
|
||||||
|
gui_name = 'Pico'
|
||||||
|
description = _('Communicate with the Pico reader.')
|
||||||
|
|
||||||
|
WINDOWS_MAIN_MEM = 'USBDISK__USER'
|
||||||
|
EBOOK_DIR_MAIN = 'Books'
|
||||||
|
FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT']
|
||||||
|
|
||||||
class IPAPYRUS(TECLAST_K3):
|
class IPAPYRUS(TECLAST_K3):
|
||||||
|
|
||||||
name = 'iPapyrus device interface'
|
name = 'iPapyrus device interface'
|
||||||
|
@ -30,9 +30,9 @@ def detect(aBuf):
|
|||||||
|
|
||||||
# Added by Kovid
|
# Added by Kovid
|
||||||
ENCODING_PATS = [
|
ENCODING_PATS = [
|
||||||
re.compile(r'<\?[^<>]+encoding=[\'"](.*?)[\'"][^<>]*>',
|
re.compile(r'<\?[^<>]+encoding\s*=\s*[\'"](.*?)[\'"][^<>]*>',
|
||||||
re.IGNORECASE),
|
re.IGNORECASE),
|
||||||
re.compile(r'''<meta\s+?[^<>]+?content=['"][^'"]*?charset=([-a-z0-9]+)[^'"]*?['"][^<>]*>''',
|
re.compile(r'''<meta\s+?[^<>]+?content\s*=\s*['"][^'"]*?charset=([-a-z0-9]+)[^'"]*?['"][^<>]*>''',
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
]
|
]
|
||||||
ENTITY_PATTERN = re.compile(r'&(\S+?);')
|
ENTITY_PATTERN = re.compile(r'&(\S+?);')
|
||||||
|
@ -14,7 +14,7 @@ from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
|
|||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
from calibre.utils.date import parse_date
|
from calibre.utils.date import parse_date
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
from calibre import extract, walk
|
from calibre import extract, walk, isbytestring, filesystem_encoding
|
||||||
from calibre.constants import __version__
|
from calibre.constants import __version__
|
||||||
|
|
||||||
DEBUG_README=u'''
|
DEBUG_README=u'''
|
||||||
@ -77,6 +77,10 @@ class Plumber(object):
|
|||||||
:param input: Path to input file.
|
:param input: Path to input file.
|
||||||
:param output: Path to output file/directory
|
: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.original_input_arg = input
|
||||||
self.input = os.path.abspath(input)
|
self.input = os.path.abspath(input)
|
||||||
self.output = os.path.abspath(output)
|
self.output = os.path.abspath(output)
|
||||||
|
@ -40,14 +40,35 @@ class FB2Input(InputFormatPlugin):
|
|||||||
accelerators):
|
accelerators):
|
||||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
from calibre.ebooks.oeb.base import XLINK_NS
|
from calibre.ebooks.oeb.base import XLINK_NS, XHTML_NS, RECOVER_PARSER
|
||||||
NAMESPACES = {'f':FB2NS, 'l':XLINK_NS}
|
NAMESPACES = {'f':FB2NS, 'l':XLINK_NS}
|
||||||
log.debug('Parsing XML...')
|
log.debug('Parsing XML...')
|
||||||
raw = stream.read()
|
raw = stream.read().replace('\0', '')
|
||||||
try:
|
try:
|
||||||
doc = etree.fromstring(raw)
|
doc = etree.fromstring(raw)
|
||||||
except etree.XMLSyntaxError:
|
except etree.XMLSyntaxError:
|
||||||
doc = etree.fromstring(raw.replace('& ', '&'))
|
try:
|
||||||
|
doc = etree.fromstring(raw, parser=RECOVER_PARSER)
|
||||||
|
except:
|
||||||
|
doc = etree.fromstring(raw.replace('& ', '&'),
|
||||||
|
parser=RECOVER_PARSER)
|
||||||
|
stylesheets = doc.xpath('//*[local-name() = "stylesheet" and @type="text/css"]')
|
||||||
|
css = ''
|
||||||
|
for s in stylesheets:
|
||||||
|
css += etree.tostring(s, encoding=unicode, method='text',
|
||||||
|
with_tail=False) + '\n\n'
|
||||||
|
if css:
|
||||||
|
import cssutils, logging
|
||||||
|
parser = cssutils.CSSParser(fetcher=None,
|
||||||
|
log=logging.getLogger('calibre.css'))
|
||||||
|
|
||||||
|
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
|
||||||
|
text = XHTML_CSS_NAMESPACE + css
|
||||||
|
log.debug('Parsing stylesheet...')
|
||||||
|
stylesheet = parser.parseString(text)
|
||||||
|
stylesheet.namespaces['h'] = XHTML_NS
|
||||||
|
css = unicode(stylesheet.cssText).replace('h|style', 'h|span')
|
||||||
|
css = re.sub(r'name\s*=\s*', 'class=', css)
|
||||||
self.extract_embedded_content(doc)
|
self.extract_embedded_content(doc)
|
||||||
log.debug('Converting XML to HTML...')
|
log.debug('Converting XML to HTML...')
|
||||||
ss = open(P('templates/fb2.xsl'), 'rb').read()
|
ss = open(P('templates/fb2.xsl'), 'rb').read()
|
||||||
@ -63,7 +84,9 @@ class FB2Input(InputFormatPlugin):
|
|||||||
for img in result.xpath('//img[@src]'):
|
for img in result.xpath('//img[@src]'):
|
||||||
src = img.get('src')
|
src = img.get('src')
|
||||||
img.set('src', self.binary_map.get(src, src))
|
img.set('src', self.binary_map.get(src, src))
|
||||||
open('index.xhtml', 'wb').write(transform.tostring(result))
|
index = transform.tostring(result)
|
||||||
|
open('index.xhtml', 'wb').write(index)
|
||||||
|
open('inline-styles.css', 'wb').write(css)
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
mi = get_metadata(stream, 'fb2')
|
mi = get_metadata(stream, 'fb2')
|
||||||
if not mi.title:
|
if not mi.title:
|
||||||
|
@ -8,88 +8,114 @@ Fetch metadata using Amazon AWS
|
|||||||
'''
|
'''
|
||||||
import sys, re
|
import sys, re
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import html
|
||||||
|
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.date import parse_date, utcnow
|
from calibre.ebooks.metadata import check_isbn
|
||||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
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):
|
def to_asin(br, isbn):
|
||||||
return '{%s}%s'%(AWS_NS, tag)
|
if len(isbn) == 13:
|
||||||
|
try:
|
||||||
class ISBNNotFound(ValueError):
|
asin = find_asin(br, isbn)
|
||||||
pass
|
except:
|
||||||
|
import traceback
|
||||||
def check_for_errors(root, isbn):
|
traceback.print_exc()
|
||||||
err = root.find('.//'+AWS('Error'))
|
asin = None
|
||||||
if err is not None:
|
else:
|
||||||
text = etree.tostring(err, method='text', pretty_print=True,
|
asin = isbn
|
||||||
encoding=unicode)
|
return asin
|
||||||
if 'AWS.InvalidParameterValue'+isbn in text:
|
|
||||||
raise ISBNNotFound(isbn)
|
|
||||||
raise Exception('Failed to get metadata with error: '\
|
|
||||||
+ text)
|
|
||||||
|
|
||||||
def get_social_metadata(title, authors, publisher, isbn):
|
def get_social_metadata(title, authors, publisher, isbn):
|
||||||
mi = MetaInformation(title, authors)
|
mi = Metadata(title, authors)
|
||||||
if isbn:
|
if not 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
|
|
||||||
|
|
||||||
return mi
|
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):
|
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
|
return 0
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -22,7 +22,8 @@ def get_metadata(stream):
|
|||||||
'xlink':XLINK_NS})
|
'xlink':XLINK_NS})
|
||||||
tostring = lambda x : etree.tostring(x, method='text',
|
tostring = lambda x : etree.tostring(x, method='text',
|
||||||
encoding=unicode).strip()
|
encoding=unicode).strip()
|
||||||
root = etree.fromstring(stream.read())
|
parser = etree.XMLParser(recover=True, no_network=True)
|
||||||
|
root = etree.fromstring(stream.read(), parser=parser)
|
||||||
authors, author_sort = [], None
|
authors, author_sort = [], None
|
||||||
for au in XPath('//fb2:author')(root):
|
for au in XPath('//fb2:author')(root):
|
||||||
fname = lname = author = None
|
fname = lname = author = None
|
||||||
|
@ -21,51 +21,62 @@ def debug(*args):
|
|||||||
prints(*args)
|
prints(*args)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def read_metadata_(task, tdir, notification=lambda x,y:x):
|
def serialize_metadata_for(formats, tdir, id_):
|
||||||
from calibre.ebooks.metadata.meta import metadata_from_formats
|
from calibre.ebooks.metadata.meta import metadata_from_formats
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
|
mi = metadata_from_formats(formats)
|
||||||
|
mi.cover = None
|
||||||
|
cdata = None
|
||||||
|
if mi.cover_data:
|
||||||
|
cdata = mi.cover_data[-1]
|
||||||
|
mi.cover_data = None
|
||||||
|
if not mi.application_id:
|
||||||
|
mi.application_id = '__calibre_dummy__'
|
||||||
|
with open(os.path.join(tdir, '%s.opf'%id_), 'wb') as f:
|
||||||
|
f.write(metadata_to_opf(mi))
|
||||||
|
if cdata:
|
||||||
|
with open(os.path.join(tdir, str(id_)), 'wb') as f:
|
||||||
|
f.write(cdata)
|
||||||
|
|
||||||
|
def read_metadata_(task, tdir, notification=lambda x,y:x):
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
for x in task:
|
for x in task:
|
||||||
try:
|
try:
|
||||||
id, formats = x
|
id_, formats = x
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
if isinstance(formats, basestring): formats = [formats]
|
if isinstance(formats, basestring): formats = [formats]
|
||||||
mi = metadata_from_formats(formats)
|
|
||||||
mi.cover = None
|
|
||||||
cdata = None
|
|
||||||
if mi.cover_data:
|
|
||||||
cdata = mi.cover_data[-1]
|
|
||||||
mi.cover_data = None
|
|
||||||
if not mi.application_id:
|
|
||||||
mi.application_id = '__calibre_dummy__'
|
|
||||||
with open(os.path.join(tdir, '%s.opf'%id), 'wb') as f:
|
|
||||||
f.write(metadata_to_opf(mi))
|
|
||||||
if cdata:
|
|
||||||
with open(os.path.join(tdir, str(id)), 'wb') as f:
|
|
||||||
f.write(cdata)
|
|
||||||
import_map = {}
|
import_map = {}
|
||||||
|
fmts = []
|
||||||
for format in formats:
|
for format in formats:
|
||||||
nfp = run_plugins_on_import(format)
|
nfp = run_plugins_on_import(format)
|
||||||
if nfp is None:
|
if not nfp or not os.access(nfp, os.R_OK):
|
||||||
nfp = format
|
nfp = format
|
||||||
nfp = os.path.abspath(nfp)
|
nfp = os.path.abspath(nfp)
|
||||||
|
fmts.append(nfp)
|
||||||
|
|
||||||
|
serialize_metadata_for(fmts, tdir, id_)
|
||||||
|
|
||||||
|
for format, nfp in zip(formats, fmts):
|
||||||
if isinstance(nfp, unicode):
|
if isinstance(nfp, unicode):
|
||||||
nfp.encode(filesystem_encoding)
|
nfp.encode(filesystem_encoding)
|
||||||
x = lambda j : os.path.abspath(os.path.normpath(os.path.normcase(j)))
|
x = lambda j : os.path.abspath(os.path.normpath(os.path.normcase(j)))
|
||||||
if x(nfp) != x(format) and os.access(nfp, os.R_OK|os.W_OK):
|
if x(nfp) != x(format) and os.access(nfp, os.R_OK|os.W_OK):
|
||||||
fmt = os.path.splitext(format)[1].replace('.', '').lower()
|
fmt = os.path.splitext(format)[1].replace('.', '').lower()
|
||||||
nfmt = os.path.splitext(nfp)[1].replace('.', '').lower()
|
nfmt = os.path.splitext(nfp)[1].replace('.', '').lower()
|
||||||
dest = os.path.join(tdir, '%s.%s'%(id, nfmt))
|
dest = os.path.join(tdir, '%s.%s'%(id_, nfmt))
|
||||||
shutil.copyfile(nfp, dest)
|
shutil.copyfile(nfp, dest)
|
||||||
import_map[fmt] = dest
|
import_map[fmt] = dest
|
||||||
os.remove(nfp)
|
os.remove(nfp)
|
||||||
if import_map:
|
if import_map:
|
||||||
with open(os.path.join(tdir, str(id)+'.import'), 'wb') as f:
|
with open(os.path.join(tdir, str(id_)+'.import'), 'wb') as f:
|
||||||
for fmt, nfp in import_map.items():
|
for fmt, nfp in import_map.items():
|
||||||
f.write(fmt+':'+nfp+'\n')
|
f.write(fmt+':'+nfp+'\n')
|
||||||
notification(0.5, id)
|
notification(0.5, id_)
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
with open(os.path.join(tdir, '%s.error'%id), 'wb') as f:
|
with open(os.path.join(tdir, '%s.error'%id_), 'wb') as f:
|
||||||
f.write(traceback.format_exc())
|
f.write(traceback.format_exc())
|
||||||
|
|
||||||
class Progress(object):
|
class Progress(object):
|
||||||
|
80
src/calibre/ebooks/metadata/xisbn.py
Normal file
80
src/calibre/ebooks/metadata/xisbn.py
Normal 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)
|
||||||
|
|
||||||
|
|
@ -27,6 +27,8 @@ TABLE_TAGS = set(['table', 'tr', 'td', 'th', 'caption'])
|
|||||||
SPECIAL_TAGS = set(['hr', 'br'])
|
SPECIAL_TAGS = set(['hr', 'br'])
|
||||||
CONTENT_TAGS = set(['img', 'hr', 'br'])
|
CONTENT_TAGS = set(['img', 'hr', 'br'])
|
||||||
|
|
||||||
|
NOT_VTAGS = HEADER_TAGS | NESTABLE_TAGS | TABLE_TAGS | SPECIAL_TAGS | \
|
||||||
|
CONTENT_TAGS
|
||||||
PAGE_BREAKS = set(['always', 'left', 'right'])
|
PAGE_BREAKS = set(['always', 'left', 'right'])
|
||||||
|
|
||||||
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
COLLAPSE = re.compile(r'[ \t\r\n\v]+')
|
||||||
@ -57,8 +59,6 @@ class FormatState(object):
|
|||||||
self.indent = 0.
|
self.indent = 0.
|
||||||
self.fsize = 3
|
self.fsize = 3
|
||||||
self.ids = set()
|
self.ids = set()
|
||||||
self.valign = 'baseline'
|
|
||||||
self.nest = False
|
|
||||||
self.italic = False
|
self.italic = False
|
||||||
self.bold = False
|
self.bold = False
|
||||||
self.strikethrough = False
|
self.strikethrough = False
|
||||||
@ -76,7 +76,6 @@ class FormatState(object):
|
|||||||
and self.italic == other.italic \
|
and self.italic == other.italic \
|
||||||
and self.bold == other.bold \
|
and self.bold == other.bold \
|
||||||
and self.href == other.href \
|
and self.href == other.href \
|
||||||
and self.valign == other.valign \
|
|
||||||
and self.preserve == other.preserve \
|
and self.preserve == other.preserve \
|
||||||
and self.family == other.family \
|
and self.family == other.family \
|
||||||
and self.bgcolor == other.bgcolor \
|
and self.bgcolor == other.bgcolor \
|
||||||
@ -224,7 +223,6 @@ class MobiMLizer(object):
|
|||||||
return
|
return
|
||||||
if not pstate or istate != pstate:
|
if not pstate or istate != pstate:
|
||||||
inline = para
|
inline = para
|
||||||
valign = istate.valign
|
|
||||||
fsize = istate.fsize
|
fsize = istate.fsize
|
||||||
href = istate.href
|
href = istate.href
|
||||||
if not href:
|
if not href:
|
||||||
@ -234,19 +232,8 @@ class MobiMLizer(object):
|
|||||||
else:
|
else:
|
||||||
inline = etree.SubElement(inline, XHTML('a'), href=href)
|
inline = etree.SubElement(inline, XHTML('a'), href=href)
|
||||||
bstate.anchor = inline
|
bstate.anchor = inline
|
||||||
if valign == 'super':
|
|
||||||
parent = inline
|
if fsize != 3:
|
||||||
if istate.nest and bstate.inline is not None:
|
|
||||||
parent = bstate.inline
|
|
||||||
istate.nest = False
|
|
||||||
inline = etree.SubElement(parent, XHTML('sup'))
|
|
||||||
elif valign == 'sub':
|
|
||||||
parent = inline
|
|
||||||
if istate.nest and bstate.inline is not None:
|
|
||||||
parent = bstate.inline
|
|
||||||
istate.nest = False
|
|
||||||
inline = etree.SubElement(parent, XHTML('sub'))
|
|
||||||
elif fsize != 3:
|
|
||||||
inline = etree.SubElement(inline, XHTML('font'),
|
inline = etree.SubElement(inline, XHTML('font'),
|
||||||
size=str(fsize))
|
size=str(fsize))
|
||||||
if istate.family == 'monospace':
|
if istate.family == 'monospace':
|
||||||
@ -279,7 +266,8 @@ class MobiMLizer(object):
|
|||||||
else:
|
else:
|
||||||
inline.append(item)
|
inline.append(item)
|
||||||
|
|
||||||
def mobimlize_elem(self, elem, stylizer, bstate, istates):
|
def mobimlize_elem(self, elem, stylizer, bstate, istates,
|
||||||
|
ignore_valign=False):
|
||||||
if not isinstance(elem.tag, basestring) \
|
if not isinstance(elem.tag, basestring) \
|
||||||
or namespace(elem.tag) != XHTML_NS:
|
or namespace(elem.tag) != XHTML_NS:
|
||||||
return
|
return
|
||||||
@ -351,15 +339,6 @@ class MobiMLizer(object):
|
|||||||
istate.family = 'sans-serif'
|
istate.family = 'sans-serif'
|
||||||
else:
|
else:
|
||||||
istate.family = 'serif'
|
istate.family = 'serif'
|
||||||
valign = style['vertical-align']
|
|
||||||
if valign in ('super', 'text-top') or asfloat(valign) > 0:
|
|
||||||
istate.nest = istate.valign in ('sub', 'super')
|
|
||||||
istate.valign = 'super'
|
|
||||||
elif valign == 'sub' or asfloat(valign) < 0:
|
|
||||||
istate.nest = istate.valign in ('sub', 'super')
|
|
||||||
istate.valign = 'sub'
|
|
||||||
else:
|
|
||||||
istate.valign = 'baseline'
|
|
||||||
if 'id' in elem.attrib:
|
if 'id' in elem.attrib:
|
||||||
istate.ids.add(elem.attrib['id'])
|
istate.ids.add(elem.attrib['id'])
|
||||||
if 'name' in elem.attrib:
|
if 'name' in elem.attrib:
|
||||||
@ -407,6 +386,30 @@ class MobiMLizer(object):
|
|||||||
text = None
|
text = None
|
||||||
else:
|
else:
|
||||||
text = COLLAPSE.sub(' ', elem.text)
|
text = COLLAPSE.sub(' ', elem.text)
|
||||||
|
valign = style['vertical-align']
|
||||||
|
not_baseline = valign in ('super', 'sub', 'text-top',
|
||||||
|
'text-bottom')
|
||||||
|
vtag = 'sup' if valign in ('super', 'text-top') else 'sub'
|
||||||
|
if not_baseline and not ignore_valign and tag not in NOT_VTAGS and not isblock:
|
||||||
|
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
|
||||||
|
vbstate = BlockState(etree.SubElement(nroot, XHTML('body')))
|
||||||
|
vbstate.para = etree.SubElement(vbstate.body, XHTML('p'))
|
||||||
|
self.mobimlize_elem(elem, stylizer, vbstate, istates,
|
||||||
|
ignore_valign=True)
|
||||||
|
if len(istates) > 0:
|
||||||
|
istates.pop()
|
||||||
|
if len(istates) == 0:
|
||||||
|
istates.append(FormatState())
|
||||||
|
at_start = bstate.para is None
|
||||||
|
if at_start:
|
||||||
|
self.mobimlize_content('span', '', bstate, istates)
|
||||||
|
parent = bstate.para if bstate.inline is None else bstate.inline
|
||||||
|
if parent is not None:
|
||||||
|
vtag = etree.SubElement(parent, XHTML(vtag))
|
||||||
|
for child in vbstate.para:
|
||||||
|
vtag.append(child)
|
||||||
|
return
|
||||||
|
|
||||||
if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS:
|
if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS:
|
||||||
self.mobimlize_content(tag, text, bstate, istates)
|
self.mobimlize_content(tag, text, bstate, istates)
|
||||||
for child in elem:
|
for child in elem:
|
||||||
@ -421,6 +424,8 @@ class MobiMLizer(object):
|
|||||||
tail = COLLAPSE.sub(' ', child.tail)
|
tail = COLLAPSE.sub(' ', child.tail)
|
||||||
if tail:
|
if tail:
|
||||||
self.mobimlize_content(tag, tail, bstate, istates)
|
self.mobimlize_content(tag, tail, bstate, istates)
|
||||||
|
|
||||||
|
|
||||||
if bstate.content and style['page-break-after'] in PAGE_BREAKS:
|
if bstate.content and style['page-break-after'] in PAGE_BREAKS:
|
||||||
bstate.pbreak = True
|
bstate.pbreak = True
|
||||||
if isblock:
|
if isblock:
|
||||||
|
@ -2043,12 +2043,16 @@ class MobiWriter(object):
|
|||||||
else :
|
else :
|
||||||
self._oeb.logger.info("chapterCount: %d" % self._chapterCount)
|
self._oeb.logger.info("chapterCount: %d" % self._chapterCount)
|
||||||
|
|
||||||
if True:
|
# Apparently the CTOC must end with a null byte
|
||||||
rec_count = len(self._ctoc_records)
|
self._ctoc.write('\0')
|
||||||
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) )
|
|
||||||
|
|
||||||
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) :
|
def _write_periodical_node(self, indxt, indices, index, offset, length, count, firstSection, lastSection) :
|
||||||
pos = 0xc0 + indxt.tell()
|
pos = 0xc0 + indxt.tell()
|
||||||
|
@ -25,6 +25,7 @@ from calibre.translations.dynamic import translate
|
|||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||||
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
|
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
|
||||||
|
from calibre import isbytestring
|
||||||
|
|
||||||
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
|
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
|
||||||
|
|
||||||
@ -404,7 +405,8 @@ class DirContainer(object):
|
|||||||
|
|
||||||
def __init__(self, path, log):
|
def __init__(self, path, log):
|
||||||
self.log = log
|
self.log = log
|
||||||
path = unicode(path)
|
if isbytestring(path):
|
||||||
|
path = path.decode(filesystem_encoding)
|
||||||
ext = os.path.splitext(path)[1].lower()
|
ext = os.path.splitext(path)[1].lower()
|
||||||
if ext == '.opf':
|
if ext == '.opf':
|
||||||
self.opfname = os.path.basename(path)
|
self.opfname = os.path.basename(path)
|
||||||
|
@ -6,8 +6,6 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import cStringIO
|
|
||||||
|
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
class RescaleImages(object):
|
class RescaleImages(object):
|
||||||
@ -19,13 +17,7 @@ class RescaleImages(object):
|
|||||||
self.rescale(qt=is_ok_to_use_qt())
|
self.rescale(qt=is_ok_to_use_qt())
|
||||||
|
|
||||||
def rescale(self, qt=True):
|
def rescale(self, qt=True):
|
||||||
from PyQt4.Qt import QImage, Qt
|
from calibre.utils.magick.draw import Image
|
||||||
from calibre.gui2 import pixmap_to_data
|
|
||||||
try:
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
PILImage
|
|
||||||
except ImportError:
|
|
||||||
import Image as PILImage
|
|
||||||
|
|
||||||
is_image_collection = getattr(self.opts, 'is_image_collection', False)
|
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, 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_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.
|
page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72.
|
||||||
|
|
||||||
for item in self.oeb.manifest:
|
for item in self.oeb.manifest:
|
||||||
if item.media_type.startswith('image'):
|
if item.media_type.startswith('image'):
|
||||||
ext = item.media_type.split('/')[-1].upper()
|
ext = item.media_type.split('/')[-1].upper()
|
||||||
@ -44,42 +37,25 @@ class RescaleImages(object):
|
|||||||
|
|
||||||
raw = item.data
|
raw = item.data
|
||||||
if not raw: continue
|
if not raw: continue
|
||||||
if qt:
|
try:
|
||||||
img = QImage(10, 10, QImage.Format_ARGB32_Premultiplied)
|
img = Image()
|
||||||
try:
|
img.load(raw)
|
||||||
if not img.loadFromData(raw): continue
|
except:
|
||||||
except:
|
continue
|
||||||
continue
|
width, height = img.size
|
||||||
width, height = img.width(), img.height()
|
|
||||||
else:
|
|
||||||
f = cStringIO.StringIO(raw)
|
|
||||||
try:
|
|
||||||
im = PILImage.open(f)
|
|
||||||
except IOError:
|
|
||||||
continue
|
|
||||||
width, height = im.size
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scaled, new_width, new_height = fit_image(width, height,
|
scaled, new_width, new_height = fit_image(width, height,
|
||||||
page_width, page_height)
|
page_width, page_height)
|
||||||
if scaled:
|
if scaled:
|
||||||
data = None
|
|
||||||
self.log('Rescaling image from %dx%d to %dx%d'%(
|
self.log('Rescaling image from %dx%d to %dx%d'%(
|
||||||
width, height, new_width, new_height), item.href)
|
width, height, new_width, new_height), item.href)
|
||||||
if qt:
|
try:
|
||||||
img = img.scaled(new_width, new_height,
|
img.size = (new_width, new_height)
|
||||||
Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
data = img.export(ext.lower())
|
||||||
data = pixmap_to_data(img, format=ext)
|
except:
|
||||||
|
self.log.exception('Failed to rescale image')
|
||||||
else:
|
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.data = data
|
||||||
item.unload_data_from_memory()
|
item.unload_data_from_memory()
|
||||||
|
|
||||||
|
@ -20,20 +20,10 @@ class SNBOutput(OutputFormatPlugin):
|
|||||||
file_type = 'snb'
|
file_type = 'snb'
|
||||||
|
|
||||||
options = set([
|
options = set([
|
||||||
# OptionRecommendation(name='newline', recommended_value='system',
|
|
||||||
# level=OptionRecommendation.LOW,
|
|
||||||
# short_switch='n', choices=TxtNewlines.NEWLINE_TYPES.keys(),
|
|
||||||
# help=_('Type of newline to use. Options are %s. Default is \'system\'. '
|
|
||||||
# 'Use \'old_mac\' for compatibility with Mac OS 9 and earlier. '
|
|
||||||
# 'For Mac OS X use \'unix\'. \'system\' will default to the newline '
|
|
||||||
# 'type used by this OS.') % sorted(TxtNewlines.NEWLINE_TYPES.keys())),
|
|
||||||
OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8',
|
OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8',
|
||||||
level=OptionRecommendation.LOW,
|
level=OptionRecommendation.LOW,
|
||||||
help=_('Specify the character encoding of the output document. ' \
|
help=_('Specify the character encoding of the output document. ' \
|
||||||
'The default is utf-8.')),
|
'The default is utf-8.')),
|
||||||
# OptionRecommendation(name='inline_toc',
|
|
||||||
# recommended_value=False, level=OptionRecommendation.LOW,
|
|
||||||
# help=_('Add Table of Contents to beginning of the book.')),
|
|
||||||
OptionRecommendation(name='snb_max_line_length',
|
OptionRecommendation(name='snb_max_line_length',
|
||||||
recommended_value=0, level=OptionRecommendation.LOW,
|
recommended_value=0, level=OptionRecommendation.LOW,
|
||||||
help=_('The maximum number of characters per line. This splits on '
|
help=_('The maximum number of characters per line. This splits on '
|
||||||
@ -41,10 +31,18 @@ class SNBOutput(OutputFormatPlugin):
|
|||||||
'the line will be broken at the space after and will exceed the '
|
'the line will be broken at the space after and will exceed the '
|
||||||
'specified value. Also, there is a minimum of 25 characters. '
|
'specified value. Also, there is a minimum of 25 characters. '
|
||||||
'Use 0 to disable line splitting.')),
|
'Use 0 to disable line splitting.')),
|
||||||
# OptionRecommendation(name='force_max_line_length',
|
OptionRecommendation(name='snb_insert_empty_line',
|
||||||
# recommended_value=False, level=OptionRecommendation.LOW,
|
recommended_value=False, level=OptionRecommendation.LOW,
|
||||||
# help=_('Force splitting on the max-line-length value when no space '
|
help=_('Specify whether or not to insert an empty line between '
|
||||||
# 'is present. Also allows max-line-length to be below the minimum')),
|
'two paragraphs.')),
|
||||||
|
OptionRecommendation(name='snb_indent_first_line',
|
||||||
|
recommended_value=True, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Specify whether or not to insert two space characters '
|
||||||
|
'to indent the first line of each paragraph.')),
|
||||||
|
OptionRecommendation(name='snb_hide_chapter_name',
|
||||||
|
recommended_value=False, level=OptionRecommendation.LOW,
|
||||||
|
help=_('Specify whether or not to hide the chapter title for each '
|
||||||
|
'chapter. Useful for image-only output (eg. comics).')),
|
||||||
])
|
])
|
||||||
|
|
||||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
@ -230,7 +228,7 @@ class SNBOutput(OutputFormatPlugin):
|
|||||||
img.load(imageData)
|
img.load(imageData)
|
||||||
(x,y) = img.size
|
(x,y) = img.size
|
||||||
if self.opts:
|
if self.opts:
|
||||||
SCREEN_Y, SCREEN_X = self.opts.output_profile.comic_screen_size
|
SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size
|
||||||
else:
|
else:
|
||||||
SCREEN_X = 540
|
SCREEN_X = 540
|
||||||
SCREEN_Y = 700
|
SCREEN_Y = 700
|
||||||
|
@ -88,7 +88,10 @@ class SNBMLizer(object):
|
|||||||
trees = { }
|
trees = { }
|
||||||
for subitem, subtitle in self.subitems:
|
for subitem, subtitle in self.subitems:
|
||||||
snbcTree = etree.Element("snbc")
|
snbcTree = etree.Element("snbc")
|
||||||
etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle
|
snbcHead = etree.SubElement(snbcTree, "head")
|
||||||
|
etree.SubElement(snbcHead, "title").text = subtitle
|
||||||
|
if self.opts and self.opts.snb_hide_chapter_name:
|
||||||
|
etree.SubElement(snbcHead, "hidetitle").text = u"true"
|
||||||
etree.SubElement(snbcTree, "body")
|
etree.SubElement(snbcTree, "body")
|
||||||
trees[subitem] = snbcTree
|
trees[subitem] = snbcTree
|
||||||
output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, ""))
|
output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, ""))
|
||||||
@ -96,27 +99,37 @@ class SNBMLizer(object):
|
|||||||
output = self.cleanup_text(u''.join(output))
|
output = self.cleanup_text(u''.join(output))
|
||||||
|
|
||||||
subitem = ''
|
subitem = ''
|
||||||
|
bodyTree = trees[subitem].find(".//body")
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
|
if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
|
||||||
line = line.strip(u' \t\n\r\u3000')
|
line = line.strip(u' \t\n\r\u3000')
|
||||||
else:
|
else:
|
||||||
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
|
etree.SubElement(bodyTree, "text").text = \
|
||||||
etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
|
etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
|
||||||
continue
|
continue
|
||||||
if len(line) != 0:
|
if len(line) != 0:
|
||||||
if line.find(CALIBRE_SNB_IMG_TAG) == 0:
|
if line.find(CALIBRE_SNB_IMG_TAG) == 0:
|
||||||
prefix = ProcessFileName(os.path.dirname(self.item.href))
|
prefix = ProcessFileName(os.path.dirname(self.item.href))
|
||||||
if prefix != '':
|
if prefix != '':
|
||||||
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
|
etree.SubElement(bodyTree, "img").text = \
|
||||||
prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):]
|
prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):]
|
||||||
else:
|
else:
|
||||||
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
|
etree.SubElement(bodyTree, "img").text = \
|
||||||
line[len(CALIBRE_SNB_IMG_TAG):]
|
line[len(CALIBRE_SNB_IMG_TAG):]
|
||||||
elif line.find(CALIBRE_SNB_BM_TAG) == 0:
|
elif line.find(CALIBRE_SNB_BM_TAG) == 0:
|
||||||
subitem = line[len(CALIBRE_SNB_BM_TAG):]
|
subitem = line[len(CALIBRE_SNB_BM_TAG):]
|
||||||
|
bodyTree = trees[subitem].find(".//body")
|
||||||
else:
|
else:
|
||||||
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
|
if self.opts and self.opts.snb_indent_first_line:
|
||||||
etree.CDATA(unicode(u'\u3000\u3000' + line))
|
prefix = u'\u3000\u3000'
|
||||||
|
else:
|
||||||
|
prefix = u''
|
||||||
|
etree.SubElement(bodyTree, "text").text = \
|
||||||
|
etree.CDATA(unicode(prefix + line))
|
||||||
|
if self.opts and self.opts.snb_insert_empty_line:
|
||||||
|
etree.SubElement(bodyTree, "text").text = \
|
||||||
|
etree.CDATA(u'')
|
||||||
|
|
||||||
return trees
|
return trees
|
||||||
|
|
||||||
def remove_newlines(self, text):
|
def remove_newlines(self, text):
|
||||||
|
@ -255,7 +255,7 @@ def error_dialog(parent, title, msg, det_msg='', show=False,
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def question_dialog(parent, title, msg, det_msg='', show_copy_button=True,
|
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,
|
d = MessageBox(QMessageBox.Question, title, msg, buttons,
|
||||||
parent, det_msg)
|
parent, det_msg)
|
||||||
d.setIconPixmap(QPixmap(I('dialog_question.png')))
|
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:
|
if not show_copy_button:
|
||||||
d.cb.setVisible(False)
|
d.cb.setVisible(False)
|
||||||
|
|
||||||
return d.exec_() == QMessageBox.Yes
|
return d.exec_() == yes_button
|
||||||
|
|
||||||
def info_dialog(parent, title, msg, det_msg='', show=False):
|
def info_dialog(parent, title, msg, det_msg='', show=False):
|
||||||
d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok,
|
d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok,
|
||||||
@ -399,6 +399,7 @@ class FileIconProvider(QFileIconProvider):
|
|||||||
'fb2' : 'fb2',
|
'fb2' : 'fb2',
|
||||||
'rtf' : 'rtf',
|
'rtf' : 'rtf',
|
||||||
'odt' : 'odt',
|
'odt' : 'odt',
|
||||||
|
'snb' : 'snb',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -9,7 +9,6 @@ from PyQt4.Qt import Qt
|
|||||||
|
|
||||||
from calibre.gui2 import Dispatcher
|
from calibre.gui2 import Dispatcher
|
||||||
from calibre.gui2.tools import fetch_scheduled_recipe
|
from calibre.gui2.tools import fetch_scheduled_recipe
|
||||||
from calibre.utils.config import dynamic
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
class FetchNewsAction(InterfaceAction):
|
class FetchNewsAction(InterfaceAction):
|
||||||
@ -60,9 +59,9 @@ class FetchNewsAction(InterfaceAction):
|
|||||||
return self.gui.job_exception(job)
|
return self.gui.job_exception(job)
|
||||||
id = self.gui.library_view.model().add_news(pt.name, arg)
|
id = self.gui.library_view.model().add_news(pt.name, arg)
|
||||||
self.gui.library_view.model().reset()
|
self.gui.library_view.model().reset()
|
||||||
sync = dynamic.get('news_to_be_synced', set([]))
|
sync = self.gui.news_to_be_synced
|
||||||
sync.add(id)
|
sync.add(id)
|
||||||
dynamic.set('news_to_be_synced', sync)
|
self.gui.news_to_be_synced = sync
|
||||||
self.scheduler.recipe_downloaded(arg)
|
self.scheduler.recipe_downloaded(arg)
|
||||||
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
|
||||||
self.gui.email_news(id)
|
self.gui.email_news(id)
|
||||||
|
@ -111,7 +111,7 @@ class ViewAction(InterfaceAction):
|
|||||||
'books at once can be slow and have a negative effect on the '
|
'books at once can be slow and have a negative effect on the '
|
||||||
'responsiveness of your computer. Once started the process '
|
'responsiveness of your computer. Once started the process '
|
||||||
'cannot be stopped until complete. Do you wish to continue?'
|
'cannot be stopped until complete. Do you wish to continue?'
|
||||||
) % num)
|
) % num, show_copy_button=False)
|
||||||
|
|
||||||
def view_folder(self, *args):
|
def view_folder(self, *args):
|
||||||
rows = self.gui.current_view().selectionModel().selectedRows()
|
rows = self.gui.current_view().selectionModel().selectedRows()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__license__ = 'GPL 3'
|
__license__ = 'GPL 3'
|
||||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from calibre.gui2.convert.snb_output_ui import Ui_Form
|
from calibre.gui2.convert.snb_output_ui import Ui_Form
|
||||||
@ -18,18 +18,9 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
|
|
||||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||||
Widget.__init__(self, parent,
|
Widget.__init__(self, parent,
|
||||||
[])
|
['snb_insert_empty_line', 'snb_indent_first_line',
|
||||||
|
'snb_hide_chapter_name',])
|
||||||
self.db, self.book_id = db, book_id
|
self.db, self.book_id = db, book_id
|
||||||
self.initialize_options(get_option, get_help, db, book_id)
|
self.initialize_options(get_option, get_help, db, book_id)
|
||||||
|
|
||||||
# default = self.opt_newline.currentText()
|
|
||||||
|
|
||||||
# global newline_model
|
|
||||||
# if newline_model is None:
|
|
||||||
# newline_model = BasicComboModel(TxtNewlines.NEWLINE_TYPES.keys())
|
|
||||||
# self.newline_model = newline_model
|
|
||||||
# self.opt_newline.setModel(self.newline_model)
|
|
||||||
|
|
||||||
# default_index = self.opt_newline.findText(default)
|
|
||||||
# system_index = self.opt_newline.findText('system')
|
|
||||||
# self.opt_newline.setCurrentIndex(default_index if default_index != -1 else system_index if system_index != -1 else 0)
|
|
||||||
|
@ -13,60 +13,41 @@
|
|||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0">
|
||||||
<!-- <item row="0" column="0"> -->
|
<item row="4" column="0">
|
||||||
<!-- <widget class="QLabel" name="label"> -->
|
<spacer name="verticalSpacer">
|
||||||
<!-- <property name="text"> -->
|
<property name="orientation">
|
||||||
<!-- <string>&Line ending style:</string> -->
|
<enum>Qt::Vertical</enum>
|
||||||
<!-- </property> -->
|
</property>
|
||||||
<!-- <property name="buddy"> -->
|
<property name="sizeHint" stdset="0">
|
||||||
<!-- <cstring>opt_newline</cstring> -->
|
<size>
|
||||||
<!-- </property> -->
|
<width>20</width>
|
||||||
<!-- </widget> -->
|
<height>40</height>
|
||||||
<!-- </item> -->
|
</size>
|
||||||
<!-- <item row="0" column="1"> -->
|
</property>
|
||||||
<!-- <widget class="QComboBox" name="opt_newline"/> -->
|
</spacer>
|
||||||
<!-- </item> -->
|
</item>
|
||||||
<!-- <item row="4" column="0"> -->
|
<item row="3" column="0">
|
||||||
<!-- <spacer name="verticalSpacer"> -->
|
<widget class="QCheckBox" name="opt_snb_hide_chapter_name">
|
||||||
<!-- <property name="orientation"> -->
|
<property name="text">
|
||||||
<!-- <enum>Qt::Vertical</enum> -->
|
<string>Hide chapter name</string>
|
||||||
<!-- </property> -->
|
</property>
|
||||||
<!-- <property name="sizeHint" stdset="0"> -->
|
</widget>
|
||||||
<!-- <size> -->
|
</item>
|
||||||
<!-- <width>20</width> -->
|
<item row="2" column="0">
|
||||||
<!-- <height>246</height> -->
|
<widget class="QCheckBox" name="opt_snb_indent_first_line">
|
||||||
<!-- </size> -->
|
<property name="text">
|
||||||
<!-- </property> -->
|
<string>Insert space before the first line for each paragraph</string>
|
||||||
<!-- </spacer> -->
|
</property>
|
||||||
<!-- </item> -->
|
</widget>
|
||||||
<!-- <item row="3" column="0" colspan="2"> -->
|
</item>
|
||||||
<!-- <widget class="QCheckBox" name="opt_inline_toc"> -->
|
<item row="1" column="0">
|
||||||
<!-- <property name="text"> -->
|
<widget class="QCheckBox" name="opt_snb_insert_empty_line">
|
||||||
<!-- <string>&Inline TOC</string> -->
|
<property name="text">
|
||||||
<!-- </property> -->
|
<string>Insert empty line between paragraphs</string>
|
||||||
<!-- </widget> -->
|
</property>
|
||||||
<!-- </item> -->
|
</widget>
|
||||||
<!-- <item row="1" column="1"> -->
|
</item>
|
||||||
<!-- <widget class="QSpinBox" name="opt_max_line_length"/> -->
|
|
||||||
<!-- </item> -->
|
|
||||||
<!-- <item row="1" column="0"> -->
|
|
||||||
<!-- <widget class="QLabel" name="label_2"> -->
|
|
||||||
<!-- <property name="text"> -->
|
|
||||||
<!-- <string>&Maximum line length:</string> -->
|
|
||||||
<!-- </property> -->
|
|
||||||
<!-- <property name="buddy"> -->
|
|
||||||
<!-- <cstring>opt_max_line_length</cstring> -->
|
|
||||||
<!-- </property> -->
|
|
||||||
<!-- </widget> -->
|
|
||||||
<!-- </item> -->
|
|
||||||
<!-- <item row="2" column="0" colspan="2"> -->
|
|
||||||
<!-- <widget class="QCheckBox" name="opt_force_max_line_length"> -->
|
|
||||||
<!-- <property name="text"> -->
|
|
||||||
<!-- <string>Force maximum line length</string> -->
|
|
||||||
<!-- </property> -->
|
|
||||||
<!-- </widget> -->
|
|
||||||
<!-- </item> -->
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
@ -489,7 +489,7 @@ class DeviceMenu(QMenu): # {{{
|
|||||||
for actions, desc in (
|
for actions, desc in (
|
||||||
(basic_actions, ''),
|
(basic_actions, ''),
|
||||||
(delete_actions, _('Send and delete from library')),
|
(delete_actions, _('Send and delete from library')),
|
||||||
(specific_actions, _('Send specific format'))
|
(specific_actions, _('Send specific format to'))
|
||||||
):
|
):
|
||||||
mdest = menu
|
mdest = menu
|
||||||
if actions is not basic_actions:
|
if actions is not basic_actions:
|
||||||
@ -1029,7 +1029,7 @@ class DeviceMixin(object): # {{{
|
|||||||
to_s = [account]
|
to_s = [account]
|
||||||
subjects = [_('News:')+' '+mi.title]
|
subjects = [_('News:')+' '+mi.title]
|
||||||
texts = [_('Attached is the')+' '+mi.title]
|
texts = [_('Attached is the')+' '+mi.title]
|
||||||
attachment_names = [mi.title+os.path.splitext(attachment)[1]]
|
attachment_names = [ascii_filename(mi.title)+os.path.splitext(attachment)[1]]
|
||||||
attachments = [attachment]
|
attachments = [attachment]
|
||||||
jobnames = ['%s:%s'%(id, mi.title)]
|
jobnames = ['%s:%s'%(id, mi.title)]
|
||||||
remove = [id] if config['delete_news_from_library_on_upload']\
|
remove = [id] if config['delete_news_from_library_on_upload']\
|
||||||
@ -1102,12 +1102,35 @@ class DeviceMixin(object): # {{{
|
|||||||
self.status_bar.show_message(_('Sending catalogs to device.'), 5000)
|
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):
|
def sync_news(self, send_ids=None, do_auto_convert=True):
|
||||||
if self.device_connected:
|
if self.device_connected:
|
||||||
del_on_upload = config['delete_news_from_library_on_upload']
|
del_on_upload = config['delete_news_from_library_on_upload']
|
||||||
settings = self.device_manager.device.settings()
|
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)]
|
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(
|
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(
|
||||||
ids, settings.format_map,
|
ids, settings.format_map,
|
||||||
@ -1139,7 +1162,7 @@ class DeviceMixin(object): # {{{
|
|||||||
for f in files:
|
for f in files:
|
||||||
f.deleted_after_upload = del_on_upload
|
f.deleted_after_upload = del_on_upload
|
||||||
if not files:
|
if not files:
|
||||||
dynamic.set('news_to_be_synced', set([]))
|
self.news_to_be_synced = set([])
|
||||||
return
|
return
|
||||||
metadata = self.library_view.model().metadata_for(ids)
|
metadata = self.library_view.model().metadata_for(ids)
|
||||||
names = []
|
names = []
|
||||||
@ -1153,7 +1176,7 @@ class DeviceMixin(object): # {{{
|
|||||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
||||||
'rb').read())
|
'rb').read())
|
||||||
dynamic.set('news_to_be_synced', set([]))
|
self.news_to_be_synced = set([])
|
||||||
if config['upload_news_to_device'] and files:
|
if config['upload_news_to_device'] and files:
|
||||||
remove = ids if del_on_upload else []
|
remove = ids if del_on_upload else []
|
||||||
space = { self.location_manager.free[0] : None,
|
space = { self.location_manager.free[0] : None,
|
||||||
@ -1347,8 +1370,9 @@ class DeviceMixin(object): # {{{
|
|||||||
# If it does not, then do it here.
|
# If it does not, then do it here.
|
||||||
if not self.set_books_in_library(self.booklists(), reset=True):
|
if not self.set_books_in_library(self.booklists(), reset=True):
|
||||||
self.upload_booklists()
|
self.upload_booklists()
|
||||||
self.book_on_device(None, reset=True)
|
with self.library_view.preserve_selected_books:
|
||||||
self.refresh_ondevice()
|
self.book_on_device(None, reset=True)
|
||||||
|
self.refresh_ondevice()
|
||||||
|
|
||||||
view = self.card_a_view if on_card == 'carda' else \
|
view = self.card_a_view if on_card == 'carda' else \
|
||||||
self.card_b_view if on_card == 'cardb' else self.memory_view
|
self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||||
|
@ -90,10 +90,15 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
row = row.row()
|
row = row.row()
|
||||||
if row == self.current_row:
|
if row == self.current_row:
|
||||||
return
|
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.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.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||||
self.current_row = row
|
self.current_row = row
|
||||||
info = self.view.model().get_book_info(row)
|
|
||||||
self.setWindowTitle(info[_('Title')])
|
self.setWindowTitle(info[_('Title')])
|
||||||
self.title.setText('<b>'+info.pop(_('Title')))
|
self.title.setText('<b>'+info.pop(_('Title')))
|
||||||
comments = info.pop(_('Comments'), '')
|
comments = info.pop(_('Comments'), '')
|
||||||
|
@ -9,11 +9,11 @@ add/remove formats
|
|||||||
import os, re, time, traceback, textwrap
|
import os, re, time, traceback, textwrap
|
||||||
|
|
||||||
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
|
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, \
|
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
||||||
choose_files, choose_images, ResizableDialog, \
|
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.metadata_single_ui import Ui_MetadataSingleDialog
|
||||||
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
@ -608,9 +608,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
|
|
||||||
def edit_tags(self):
|
def edit_tags(self):
|
||||||
if self.tags.text() != self.original_tags:
|
if self.tags.text() != self.original_tags:
|
||||||
error_dialog(self, _('Cannot use tag editor'),
|
if question_dialog(self, _('Tags changed'),
|
||||||
_('The tags editor cannot be used if you have modified the tags')).exec_()
|
_('You have changed the tags. In order to use the tags'
|
||||||
return
|
' 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 = TagEditor(self, self.db, self.row)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == QDialog.Accepted:
|
if d.result() == QDialog.Accepted:
|
||||||
@ -764,6 +771,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.series.setCurrentIndex(i)
|
self.series.setCurrentIndex(i)
|
||||||
break
|
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):
|
def accept(self):
|
||||||
cf = getattr(self, 'cover_fetcher', None)
|
cf = getattr(self, 'cover_fetcher', None)
|
||||||
@ -787,11 +798,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
notify=False, commit=False)
|
notify=False, commit=False)
|
||||||
self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
|
self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
|
self.apply_tags()
|
||||||
self.db.set_publisher(self.id,
|
self.db.set_publisher(self.id,
|
||||||
unicode(self.publisher.currentText()).strip(),
|
unicode(self.publisher.currentText()).strip(),
|
||||||
notify=False, commit=False)
|
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,
|
self.db.set_series(self.id,
|
||||||
unicode(self.series.currentText()).strip(), notify=False,
|
unicode(self.series.currentText()).strip(), notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
|
@ -374,6 +374,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if isinstance(index, int):
|
if isinstance(index, int):
|
||||||
index = self.index(index, 0)
|
index = self.index(index, 0)
|
||||||
data = self.current_changed(index, None, False)
|
data = self.current_changed(index, None, False)
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
row = index.row()
|
row = index.row()
|
||||||
data[_('Title')] = self.db.title(row)
|
data[_('Title')] = self.db.title(row)
|
||||||
au = self.db.authors(row)
|
au = self.db.authors(row)
|
||||||
|
@ -22,6 +22,26 @@ from calibre.gui2.library import DEFAULT_SORT
|
|||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre import force_unicode
|
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): # {{{
|
class BooksView(QTableView): # {{{
|
||||||
|
|
||||||
files_dropped = pyqtSignal(object)
|
files_dropped = pyqtSignal(object)
|
||||||
@ -58,6 +78,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
||||||
|
self.preserve_selected_books = PreserveSelection(self)
|
||||||
|
|
||||||
# {{{ Column Header setup
|
# {{{ Column Header setup
|
||||||
self.can_add_columns = True
|
self.can_add_columns = True
|
||||||
@ -613,6 +634,16 @@ class BooksView(QTableView): # {{{
|
|||||||
sel.select(m.index(row, 0), m.index(row, max_col))
|
sel.select(m.index(row, 0), m.index(row, max_col))
|
||||||
sm.select(sel, sm.ClearAndSelect)
|
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):
|
def close(self):
|
||||||
self._model.close()
|
self._model.close()
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
|
|||||||
QTimer, QRect
|
QTimer, QRect
|
||||||
|
|
||||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, gprefs
|
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.gui2.filename_pattern_ui import Ui_Form
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.utils.fonts import fontconfig
|
from calibre.utils.fonts import fontconfig
|
||||||
@ -303,7 +303,8 @@ class FontFamilyModel(QAbstractListModel):
|
|||||||
return NONE
|
return NONE
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return QVariant(family)
|
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 QVariant(QFont(family))
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
@ -219,6 +219,13 @@ class EZReaderPP(HanlinV5):
|
|||||||
manufacturer = 'Astak'
|
manufacturer = 'Astak'
|
||||||
id = 'ezreader_pp'
|
id = 'ezreader_pp'
|
||||||
|
|
||||||
|
class Bambook(Device):
|
||||||
|
|
||||||
|
name = 'Sanda Bambook'
|
||||||
|
output_format = 'SNB'
|
||||||
|
manufacturer = 'Sanda'
|
||||||
|
id = 'bambook'
|
||||||
|
output_profile = 'bambook'
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def get_devices():
|
def get_devices():
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
''' Code to manage ebook library'''
|
''' Code to manage ebook library'''
|
||||||
|
|
||||||
def db():
|
def db(path=None):
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
return LibraryDatabase2(prefs['library_path'])
|
return LibraryDatabase2(path if path else prefs['library_path'])
|
||||||
|
@ -44,6 +44,10 @@ def server_config(defaults=None):
|
|||||||
'by first letter when there are more than this number '
|
'by first letter when there are more than this number '
|
||||||
'of items. Default: %default. Set to a large number '
|
'of items. Default: %default. Set to a large number '
|
||||||
'to disable grouping.'))
|
'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
|
return c
|
||||||
|
|
||||||
def custom_fields_to_display(db):
|
def custom_fields_to_display(db):
|
||||||
|
@ -28,16 +28,21 @@ from calibre.library.server.browse import BrowseServer
|
|||||||
|
|
||||||
class DispatchController(object): # {{{
|
class DispatchController(object): # {{{
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, prefix, wsgi=False):
|
||||||
self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
|
self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
|
||||||
self.funcs = []
|
self.funcs = []
|
||||||
self.seen = set([])
|
self.seen = set([])
|
||||||
|
self.prefix = prefix if prefix else ''
|
||||||
|
if wsgi:
|
||||||
|
self.prefix = ''
|
||||||
|
|
||||||
def __call__(self, name, route, func, **kwargs):
|
def __call__(self, name, route, func, **kwargs):
|
||||||
if name in self.seen:
|
if name in self.seen:
|
||||||
raise NameError('Route name: '+ repr(name) + ' already used')
|
raise NameError('Route name: '+ repr(name) + ' already used')
|
||||||
self.seen.add(name)
|
self.seen.add(name)
|
||||||
kwargs['action'] = 'f_%d'%len(self.funcs)
|
kwargs['action'] = 'f_%d'%len(self.funcs)
|
||||||
|
if route != '/':
|
||||||
|
route = self.prefix + route
|
||||||
self.dispatcher.connect(name, route, self, **kwargs)
|
self.dispatcher.connect(name, route, self, **kwargs)
|
||||||
self.funcs.append(expose(func))
|
self.funcs.append(expose(func))
|
||||||
|
|
||||||
@ -55,16 +60,17 @@ class DispatchController(object): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class BonJour(SimplePlugin):
|
class BonJour(SimplePlugin): # {{{
|
||||||
|
|
||||||
def __init__(self, engine, port=8080):
|
def __init__(self, engine, port=8080, prefix=''):
|
||||||
SimplePlugin.__init__(self, engine)
|
SimplePlugin.__init__(self, engine)
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
try:
|
try:
|
||||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||||
self.port, {'path':'/stanza'})
|
self.port, {'path':self.prefix+'/stanza'})
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
cherrypy.log.error('Failed to start BonJour:')
|
cherrypy.log.error('Failed to start BonJour:')
|
||||||
@ -85,13 +91,16 @@ class BonJour(SimplePlugin):
|
|||||||
|
|
||||||
cherrypy.engine.bonjour = BonJour(cherrypy.engine)
|
cherrypy.engine.bonjour = BonJour(cherrypy.engine)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
||||||
BrowseServer):
|
BrowseServer):
|
||||||
|
|
||||||
server_name = __appname__ + '/' + __version__
|
server_name = __appname__ + '/' + __version__
|
||||||
|
|
||||||
def __init__(self, db, opts, embedded=False, show_tracebacks=True):
|
def __init__(self, db, opts, embedded=False, show_tracebacks=True,
|
||||||
|
wsgi=False):
|
||||||
|
self.is_wsgi = bool(wsgi)
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.embedded = embedded
|
self.embedded = embedded
|
||||||
self.state_callback = None
|
self.state_callback = None
|
||||||
@ -102,6 +111,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
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.port = opts.port
|
||||||
|
cherrypy.engine.bonjour.prefix = opts.url_prefix
|
||||||
|
|
||||||
Cache.__init__(self)
|
Cache.__init__(self)
|
||||||
|
|
||||||
@ -118,25 +128,36 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
'server.socket_timeout' : opts.timeout, #seconds
|
'server.socket_timeout' : opts.timeout, #seconds
|
||||||
'server.thread_pool' : opts.thread_pool, # number of threads
|
'server.thread_pool' : opts.thread_pool, # number of threads
|
||||||
})
|
})
|
||||||
if embedded:
|
if embedded or wsgi:
|
||||||
cherrypy.config.update({'engine.SIGHUP' : None,
|
cherrypy.config.update({'engine.SIGHUP' : None,
|
||||||
'engine.SIGTERM' : None,})
|
'engine.SIGTERM' : None,})
|
||||||
self.config = {'global': {
|
self.config = {}
|
||||||
'tools.gzip.on' : True,
|
|
||||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css'],
|
|
||||||
}}
|
|
||||||
if opts.password:
|
|
||||||
self.config['/'] = {
|
|
||||||
'tools.digest_auth.on' : True,
|
|
||||||
'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'),
|
|
||||||
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.exception = None
|
self.exception = None
|
||||||
self.setup_loggers()
|
if not wsgi:
|
||||||
cherrypy.engine.bonjour.subscribe()
|
self.setup_loggers()
|
||||||
|
cherrypy.engine.bonjour.subscribe()
|
||||||
|
self.config['global'] = {
|
||||||
|
'tools.gzip.on' : True,
|
||||||
|
'tools.gzip.mime_types': ['text/html', 'text/plain',
|
||||||
|
'text/xml', 'text/javascript', 'text/css'],
|
||||||
|
}
|
||||||
|
if opts.password:
|
||||||
|
self.config['/'] = {
|
||||||
|
'tools.digest_auth.on' : True,
|
||||||
|
'tools.digest_auth.realm' : (
|
||||||
|
_('Password to access your calibre library. Username is ')
|
||||||
|
+ opts.username.strip()),
|
||||||
|
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__dispatcher__ = DispatchController(self.opts.url_prefix, wsgi)
|
||||||
|
for x in self.__class__.__bases__:
|
||||||
|
if hasattr(x, 'add_routes'):
|
||||||
|
x.add_routes(self, self.__dispatcher__)
|
||||||
|
root_conf = self.config.get('/', {})
|
||||||
|
root_conf['request.dispatch'] = self.__dispatcher__.dispatcher
|
||||||
|
self.config['/'] = root_conf
|
||||||
|
|
||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -177,14 +198,6 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
d = DispatchController()
|
|
||||||
for x in self.__class__.__bases__:
|
|
||||||
if hasattr(x, 'add_routes'):
|
|
||||||
x.add_routes(self, d)
|
|
||||||
root_conf = self.config.get('/', {})
|
|
||||||
root_conf['request.dispatch'] = d.dispatcher
|
|
||||||
self.config['/'] = root_conf
|
|
||||||
|
|
||||||
cherrypy.tree.mount(root=None, config=self.config)
|
cherrypy.tree.mount(root=None, config=self.config)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -22,7 +22,7 @@ from calibre.library.comments import comments_to_html
|
|||||||
from calibre.library.server import custom_fields_to_display
|
from calibre.library.server import custom_fields_to_display
|
||||||
from calibre.library.field_metadata import category_icon_map
|
from calibre.library.field_metadata import category_icon_map
|
||||||
|
|
||||||
def render_book_list(ids, suffix=''): # {{{
|
def render_book_list(ids, prefix, suffix=''): # {{{
|
||||||
pages = []
|
pages = []
|
||||||
num = len(ids)
|
num = len(ids)
|
||||||
pos = 0
|
pos = 0
|
||||||
@ -35,11 +35,11 @@ def render_book_list(ids, suffix=''): # {{{
|
|||||||
page_template = u'''\
|
page_template = u'''\
|
||||||
<div class="page" id="page{0}">
|
<div class="page" id="page{0}">
|
||||||
<div class="load_data" title="{1}">
|
<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="start" title="{start}"></span>
|
||||||
<span class="end" title="{end}"></span>
|
<span class="end" title="{end}"></span>
|
||||||
</div>
|
</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 class="loaded"></div>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
@ -49,7 +49,7 @@ def render_book_list(ids, suffix=''): # {{{
|
|||||||
ld = xml(json.dumps(pg), True)
|
ld = xml(json.dumps(pg), True)
|
||||||
rpages.append(page_template.format(i, ld,
|
rpages.append(page_template.format(i, ld,
|
||||||
xml(_('Loading, please wait')) + '…',
|
xml(_('Loading, please wait')) + '…',
|
||||||
start=pos+1, end=pos+len(pg)))
|
start=pos+1, end=pos+len(pg), prefix=prefix))
|
||||||
rpages = u'\n\n'.join(rpages)
|
rpages = u'\n\n'.join(rpages)
|
||||||
|
|
||||||
templ = u'''\
|
templ = u'''\
|
||||||
@ -91,7 +91,7 @@ def utf8(x): # {{{
|
|||||||
return 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:
|
if rating < 0.1:
|
||||||
return '', ''
|
return '', ''
|
||||||
added = 0
|
added = 0
|
||||||
@ -108,15 +108,15 @@ def render_rating(rating, container='span', prefix=None): # {{{
|
|||||||
elif n >= 0.9:
|
elif n >= 0.9:
|
||||||
x = 'on'
|
x = 'on'
|
||||||
ans.append(
|
ans.append(
|
||||||
u'<img alt="{0}" title="{0}" src="/static/star-{1}.png" />'.format(
|
u'<img alt="{0}" title="{0}" src="{2}/static/star-{1}.png" />'.format(
|
||||||
rstring, x))
|
rstring, x, url_prefix))
|
||||||
added += 1
|
added += 1
|
||||||
ans.append('</%s>'%container)
|
ans.append('</%s>'%container)
|
||||||
return u''.join(ans), rstring
|
return u''.join(ans), rstring
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def get_category_items(category, items, restriction, datatype): # {{{
|
def get_category_items(category, items, restriction, datatype, prefix): # {{{
|
||||||
|
|
||||||
if category == 'search':
|
if category == 'search':
|
||||||
items = [x for x in items if x.name != restriction]
|
items = [x for x in items if x.name != restriction]
|
||||||
@ -125,8 +125,8 @@ def get_category_items(category, items, restriction, datatype): # {{{
|
|||||||
templ = (u'<div title="{4}" class="category-item">'
|
templ = (u'<div title="{4}" class="category-item">'
|
||||||
'<div class="category-name">{0}</div><div>{1}</div>'
|
'<div class="category-name">{0}</div><div>{1}</div>'
|
||||||
'<div>{2}'
|
'<div>{2}'
|
||||||
'<span class="href">{3}</span></div></div>')
|
'<span class="href">{5}{3}</span></div></div>')
|
||||||
rating, rstring = render_rating(i.avg_rating)
|
rating, rstring = render_rating(i.avg_rating, prefix)
|
||||||
name = xml(i.name)
|
name = xml(i.name)
|
||||||
if datatype == 'rating':
|
if datatype == 'rating':
|
||||||
name = xml(_('%d stars')%int(i.avg_rating))
|
name = xml(_('%d stars')%int(i.avg_rating))
|
||||||
@ -142,7 +142,7 @@ def get_category_items(category, items, restriction, datatype): # {{{
|
|||||||
q = category
|
q = category
|
||||||
href = '/browse/matches/%s/%s'%(quote(q), quote(id_))
|
href = '/browse/matches/%s/%s'%(quote(q), quote(id_))
|
||||||
return templ.format(xml(name), rating,
|
return templ.format(xml(name), rating,
|
||||||
xml(desc), xml(href), rstring)
|
xml(desc), xml(href), rstring, prefix)
|
||||||
|
|
||||||
items = list(map(item, items))
|
items = list(map(item, items))
|
||||||
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
|
return '\n'.join(['<div class="category-container">'] + items + ['</div>'])
|
||||||
@ -243,6 +243,7 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
||||||
ans = ans.replace('{sort_cookie_name}', scn)
|
ans = ans.replace('{sort_cookie_name}', scn)
|
||||||
|
ans = ans.replace('{prefix}', self.opts.url_prefix)
|
||||||
opts = ['<option %svalue="%s">%s</option>' % (
|
opts = ['<option %svalue="%s">%s</option>' % (
|
||||||
'selected="selected" ' if k==sort else '',
|
'selected="selected" ' if k==sort else '',
|
||||||
xml(k), xml(n), ) for k, n in
|
xml(k), xml(n), ) for k, n in
|
||||||
@ -258,15 +259,14 @@ class BrowseServer(object):
|
|||||||
ans = ans.replace('{initial_search}', initial_search)
|
ans = ans.replace('{initial_search}', initial_search)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
return self.__browse_template__
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def browse_summary_template(self):
|
def browse_summary_template(self):
|
||||||
if not hasattr(self, '__browse_summary_template__') or \
|
if not hasattr(self, '__browse_summary_template__') or \
|
||||||
self.opts.develop:
|
self.opts.develop:
|
||||||
self.__browse_summary_template__ = \
|
self.__browse_summary_template__ = \
|
||||||
P('content_server/browse/summary.html', data=True).decode('utf-8')
|
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
|
@property
|
||||||
def browse_details_template(self):
|
def browse_details_template(self):
|
||||||
@ -274,7 +274,8 @@ class BrowseServer(object):
|
|||||||
self.opts.develop:
|
self.opts.develop:
|
||||||
self.__browse_details_template__ = \
|
self.__browse_details_template__ = \
|
||||||
P('content_server/browse/details.html', data=True).decode('utf-8')
|
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)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -334,11 +335,11 @@ class BrowseServer(object):
|
|||||||
icon = 'blank.png'
|
icon = 'blank.png'
|
||||||
cats.append((meta['name'], category, icon))
|
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="label">{0}</span>'
|
||||||
'<span class="url">/browse/category/{1}</span></li>')
|
'<span class="url">{3}/browse/category/{1}</span></li>')
|
||||||
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
||||||
src='/browse/icon/'+z)
|
self.opts.url_prefix, src='/browse/icon/'+z)
|
||||||
for x, y, z in cats]
|
for x, y, z in cats]
|
||||||
|
|
||||||
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
main = '<div class="toplevel"><h3>{0}</h3><ul>{1}</ul></div>'\
|
||||||
@ -378,7 +379,8 @@ class BrowseServer(object):
|
|||||||
if len(items) <= self.opts.max_opds_ungrouped_items:
|
if len(items) <= self.opts.max_opds_ungrouped_items:
|
||||||
script = 'false'
|
script = 'false'
|
||||||
items = get_category_items(category, items,
|
items = get_category_items(category, items,
|
||||||
self.search_restriction_name, datatype)
|
self.search_restriction_name, datatype,
|
||||||
|
self.opts.url_prefix)
|
||||||
else:
|
else:
|
||||||
getter = lambda x: unicode(getattr(x, 'sort', x.name))
|
getter = lambda x: unicode(getattr(x, 'sort', x.name))
|
||||||
starts = set([])
|
starts = set([])
|
||||||
@ -393,12 +395,13 @@ class BrowseServer(object):
|
|||||||
getter(y).upper().startswith(x)])
|
getter(y).upper().startswith(x)])
|
||||||
items = [(u'<h3 title="{0}">{0} <span>[{2}]</span></h3><div>'
|
items = [(u'<h3 title="{0}">{0} <span>[{2}]</span></h3><div>'
|
||||||
u'<div class="loaded" style="display:none"></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'<div class="loading"><img alt="{1}" src="{4}/static/loading.gif" /><em>{1}</em></div>'
|
||||||
u'<span class="load_href">{3}</span></div>').format(
|
u'<span class="load_href">{4}{3}</span></div>').format(
|
||||||
xml(s, True),
|
xml(s, True),
|
||||||
xml(_('Loading, please wait'))+'…',
|
xml(_('Loading, please wait'))+'…',
|
||||||
unicode(c),
|
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()]
|
for s, c in category_groups.items()]
|
||||||
items = '\n\n'.join(items)
|
items = '\n\n'.join(items)
|
||||||
items = u'<div id="groups">\n{0}</div>'.format(items)
|
items = u'<div id="groups">\n{0}</div>'.format(items)
|
||||||
@ -410,13 +413,13 @@ class BrowseServer(object):
|
|||||||
main = u'''
|
main = u'''
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<h3>{0}</h3>
|
<h3>{0}</h3>
|
||||||
<a class="navlink" href="/browse"
|
<a class="navlink" href="{3}/browse"
|
||||||
title="{2}">{2} ↑</a>
|
title="{2}">{2} ↑</a>
|
||||||
{1}
|
{1}
|
||||||
</div>
|
</div>
|
||||||
'''.format(
|
'''.format(
|
||||||
xml(_('Browsing by')+': ' + category_name), items,
|
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,
|
return self.browse_template(sort).format(title=category_name,
|
||||||
script=script, main=main)
|
script=script, main=main)
|
||||||
@ -449,19 +452,23 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
sort = self.browse_sort_categories(entries, sort)
|
sort = self.browse_sort_categories(entries, sort)
|
||||||
entries = get_category_items(category, entries,
|
entries = get_category_items(category, entries,
|
||||||
self.search_restriction_name, datatype)
|
self.search_restriction_name, datatype,
|
||||||
|
self.opts.url_prefix)
|
||||||
return json.dumps(entries, ensure_ascii=False)
|
return json.dumps(entries, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
@Endpoint()
|
@Endpoint()
|
||||||
def browse_catalog(self, category=None, category_sort=None):
|
def browse_catalog(self, category=None, category_sort=None):
|
||||||
'Entry point for top-level, categories and sub-categories'
|
'Entry point for top-level, categories and sub-categories'
|
||||||
|
prefix = '' if self.is_wsgi else self.opts.url_prefix
|
||||||
if category == None:
|
if category == None:
|
||||||
ans = self.browse_toplevel()
|
ans = self.browse_toplevel()
|
||||||
elif category == 'newest':
|
elif category == 'newest':
|
||||||
raise cherrypy.InternalRedirect('/browse/matches/newest/dummy')
|
raise cherrypy.InternalRedirect(prefix +
|
||||||
|
'/browse/matches/newest/dummy')
|
||||||
elif category == 'allbooks':
|
elif category == 'allbooks':
|
||||||
raise cherrypy.InternalRedirect('/browse/matches/allbooks/dummy')
|
raise cherrypy.InternalRedirect(prefix +
|
||||||
|
'/browse/matches/allbooks/dummy')
|
||||||
else:
|
else:
|
||||||
ans = self.browse_category(category, category_sort)
|
ans = self.browse_category(category, category_sort)
|
||||||
|
|
||||||
@ -532,7 +539,8 @@ class BrowseServer(object):
|
|||||||
list_sort = category
|
list_sort = category
|
||||||
sort = self.browse_sort_book_list(items, list_sort)
|
sort = self.browse_sort_book_list(items, list_sort)
|
||||||
ids = [x[0] for x in items]
|
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(
|
return self.browse_template(sort, category=False).format(
|
||||||
title=_('Books in') + " " +category_name,
|
title=_('Books in') + " " +category_name,
|
||||||
@ -580,17 +588,18 @@ class BrowseServer(object):
|
|||||||
if fmts and fmt:
|
if fmts and fmt:
|
||||||
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
|
other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
|
||||||
if other_fmts:
|
if other_fmts:
|
||||||
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
||||||
.format(f, fname, id_, f.upper()) for f in
|
.format(f, fname, id_, f.upper(),
|
||||||
|
self.opts.url_prefix) for f in
|
||||||
other_fmts]
|
other_fmts]
|
||||||
ofmts = ', '.join(ofmts)
|
ofmts = ', '.join(ofmts)
|
||||||
args['other_formats'] = u'<strong>%s: </strong>' % \
|
args['other_formats'] = u'<strong>%s: </strong>' % \
|
||||||
_('Other formats') + ofmts
|
_('Other formats') + ofmts
|
||||||
|
|
||||||
args['details_href'] = '/browse/details/'+str(id_)
|
args['details_href'] = self.opts.url_prefix + '/browse/details/'+str(id_)
|
||||||
|
|
||||||
if fmt:
|
if fmt:
|
||||||
href = '/get/%s/%s_%d.%s'%(
|
href = self.opts.url_prefix + '/get/%s/%s_%d.%s'%(
|
||||||
fmt, fname, id_, fmt)
|
fmt, fname, id_, fmt)
|
||||||
rt = xml(_('Read %s in the %s format')%(args['title'],
|
rt = xml(_('Read %s in the %s format')%(args['title'],
|
||||||
fmt.upper()), True)
|
fmt.upper()), True)
|
||||||
@ -603,7 +612,8 @@ class BrowseServer(object):
|
|||||||
args['comments'] = comments_to_html(mi.comments)
|
args['comments'] = comments_to_html(mi.comments)
|
||||||
args['stars'] = ''
|
args['stars'] = ''
|
||||||
if mi.rating:
|
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']:
|
if args['tags']:
|
||||||
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
|
args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
|
||||||
args['tags']
|
args['tags']
|
||||||
@ -628,8 +638,9 @@ class BrowseServer(object):
|
|||||||
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
|
args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
|
||||||
args['formats'] = ''
|
args['formats'] = ''
|
||||||
if fmts:
|
if fmts:
|
||||||
ofmts = [u'<a href="/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'\
|
||||||
.format(fmt, fname, id_, fmt.upper()) for fmt in
|
.format(fmt, fname, id_, fmt.upper(),
|
||||||
|
self.opts.url_prefix) for fmt in
|
||||||
fmts]
|
fmts]
|
||||||
ofmts = ', '.join(ofmts)
|
ofmts = ', '.join(ofmts)
|
||||||
args['formats'] = ofmts
|
args['formats'] = ofmts
|
||||||
@ -648,7 +659,8 @@ class BrowseServer(object):
|
|||||||
continue
|
continue
|
||||||
if m['datatype'] == 'rating':
|
if m['datatype'] == 'rating':
|
||||||
r = u'<strong>%s: </strong>'%xml(m['name']) + \
|
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:
|
else:
|
||||||
r = u'<strong>%s: </strong>'%xml(m['name']) + \
|
r = u'<strong>%s: </strong>'%xml(m['name']) + \
|
||||||
args[field]
|
args[field]
|
||||||
@ -704,7 +716,8 @@ class BrowseServer(object):
|
|||||||
items = [self.db.data._data[x] for x in ids]
|
items = [self.db.data._data[x] for x in ids]
|
||||||
sort = self.browse_sort_book_list(items, list_sort)
|
sort = self.browse_sort_book_list(items, list_sort)
|
||||||
ids = [x[0] for x in items]
|
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(
|
return self.browse_template(sort, category=False, initial_search=query).format(
|
||||||
title=_('Matching books'),
|
title=_('Matching books'),
|
||||||
script='booklist();', main=html)
|
script='booklist();', main=html)
|
||||||
|
@ -103,7 +103,11 @@ class ContentServer(object):
|
|||||||
if self.opts.develop:
|
if self.opts.develop:
|
||||||
lm = fromtimestamp(os.stat(path).st_mtime)
|
lm = fromtimestamp(os.stat(path).st_mtime)
|
||||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(lm)
|
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):
|
def index(self, **kwargs):
|
||||||
'The / URL'
|
'The / URL'
|
||||||
@ -127,7 +131,8 @@ class ContentServer(object):
|
|||||||
return self.browse_toplevel()
|
return self.browse_toplevel()
|
||||||
|
|
||||||
def old(self, **kwargs):
|
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 {{{
|
# Actually get content from the database {{{
|
||||||
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80):
|
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80):
|
||||||
|
@ -24,8 +24,30 @@ def stop_threaded_server(server):
|
|||||||
server.exit()
|
server.exit()
|
||||||
server.thread = None
|
server.thread = None
|
||||||
|
|
||||||
|
def create_wsgi_app(path_to_library=None, prefix=''):
|
||||||
|
'WSGI entry point'
|
||||||
|
from calibre.library import db
|
||||||
|
cherrypy.config.update({'environment': 'embedded'})
|
||||||
|
db = db(path_to_library)
|
||||||
|
parser = option_parser()
|
||||||
|
opts, args = parser.parse_args(['calibre-server'])
|
||||||
|
opts.url_prefix = prefix
|
||||||
|
server = LibraryServer(db, opts, wsgi=True, show_tracebacks=True)
|
||||||
|
return cherrypy.Application(server, script_name=None, config=server.config)
|
||||||
|
|
||||||
def option_parser():
|
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,
|
parser.add_option('--with-library', default=None,
|
||||||
help=_('Path to the library folder to serve with the content server'))
|
help=_('Path to the library folder to serve with the content server'))
|
||||||
parser.add_option('--pidfile', default=None,
|
parser.add_option('--pidfile', default=None,
|
||||||
|
@ -26,9 +26,9 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def build_search_box(num, search, sort, order): # {{{
|
def build_search_box(num, search, sort, order, prefix): # {{{
|
||||||
div = DIV(id='search_box')
|
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')
|
form.set('accept-charset', 'UTF-8')
|
||||||
|
|
||||||
div.append(form)
|
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):
|
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
|
||||||
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
|
prefix):
|
||||||
|
logo = DIV(IMG(src=prefix+'/static/calibre.png', alt=__appname__), id='logo')
|
||||||
|
|
||||||
search_box = build_search_box(num, search, sort, order)
|
search_box = build_search_box(num, search, sort, order, prefix)
|
||||||
navigation = build_navigation(start, num, total, url_base)
|
navigation = build_navigation(start, num, total, prefix+url_base)
|
||||||
bookt = TABLE(id='listing')
|
bookt = TABLE(id='listing')
|
||||||
|
|
||||||
body = BODY(
|
body = BODY(
|
||||||
@ -107,7 +108,8 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
|||||||
# Book list {{{
|
# Book list {{{
|
||||||
for book in books:
|
for book in books:
|
||||||
thumbnail = TD(
|
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']),
|
book['id']),
|
||||||
CLASS('thumbnail'))
|
CLASS('thumbnail'))
|
||||||
|
|
||||||
@ -118,8 +120,8 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
|||||||
s = SPAN(
|
s = SPAN(
|
||||||
A(
|
A(
|
||||||
fmt.lower(),
|
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)
|
book['id'], fmt.lower())
|
||||||
),
|
),
|
||||||
CLASS('button'))
|
CLASS('button'))
|
||||||
s.tail = u''
|
s.tail = u''
|
||||||
@ -154,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
|||||||
TITLE(__appname__ + ' Library'),
|
TITLE(__appname__ + ' Library'),
|
||||||
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
|
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
|
||||||
type='image/x-icon'),
|
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
|
), # End head
|
||||||
body
|
body
|
||||||
) # End html
|
) # End html
|
||||||
@ -174,7 +176,9 @@ class MobileServer(object):
|
|||||||
cherrypy.response.headers['Content-Type'] = 'text/css; charset=utf-8'
|
cherrypy.response.headers['Content-Type'] = 'text/css; charset=utf-8'
|
||||||
updated = utcfromtimestamp(os.stat(path).st_mtime)
|
updated = utcfromtimestamp(os.stat(path).st_mtime)
|
||||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
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='',
|
def mobile(self, start='1', num='25', sort='date', search='',
|
||||||
_=None, order='descending'):
|
_=None, order='descending'):
|
||||||
@ -259,7 +263,8 @@ class MobileServer(object):
|
|||||||
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
||||||
|
|
||||||
return html.tostring(build_index(books, num, search, sort, order,
|
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,
|
encoding='utf-8', include_meta_content_type=True,
|
||||||
pretty_print=True)
|
pretty_print=True)
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
|
|||||||
link
|
link
|
||||||
)
|
)
|
||||||
|
|
||||||
def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS):
|
def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix):
|
||||||
FM = db.FIELD_MAP
|
FM = db.FIELD_MAP
|
||||||
title = item[FM['title']]
|
title = item[FM['title']]
|
||||||
if not title:
|
if not title:
|
||||||
@ -185,16 +185,16 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS):
|
|||||||
for fmt in formats.split(','):
|
for fmt in formats.split(','):
|
||||||
fmt = fmt.lower()
|
fmt = fmt.lower()
|
||||||
mt = guess_type('a.'+fmt)[0]
|
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:
|
if mt:
|
||||||
link = E.link(type=mt, href=href)
|
link = E.link(type=mt, href=href)
|
||||||
if version > 0:
|
if version > 0:
|
||||||
link.set('rel', "http://opds-spec.org/acquisition")
|
link.set('rel', "http://opds-spec.org/acquisition")
|
||||||
ans.append(link)
|
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
|
rel="x-stanza-cover-image" if version == 0 else
|
||||||
"http://opds-spec.org/cover"))
|
"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
|
rel="x-stanza-cover-image-thumbnail" if version == 0 else
|
||||||
"http://opds-spec.org/thumbnail"))
|
"http://opds-spec.org/thumbnail"))
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ class NavFeed(Feed):
|
|||||||
class AcquisitionFeed(NavFeed):
|
class AcquisitionFeed(NavFeed):
|
||||||
|
|
||||||
def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
|
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)
|
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
|
||||||
CFM = db.field_metadata
|
CFM = db.field_metadata
|
||||||
CKEYS = [key for key in sorted(custom_fields_to_display(db),
|
CKEYS = [key for key in sorted(custom_fields_to_display(db),
|
||||||
@ -283,7 +283,7 @@ class AcquisitionFeed(NavFeed):
|
|||||||
CFM[y]['name'].lower()))]
|
CFM[y]['name'].lower()))]
|
||||||
for item in items:
|
for item in items:
|
||||||
self.root.append(ACQUISITION_ENTRY(item, version, db, updated,
|
self.root.append(ACQUISITION_ENTRY(item, version, db, updated,
|
||||||
CFM, CKEYS))
|
CFM, CKEYS, prefix))
|
||||||
|
|
||||||
class CategoryFeed(NavFeed):
|
class CategoryFeed(NavFeed):
|
||||||
|
|
||||||
@ -360,7 +360,8 @@ class OPDSServer(object):
|
|||||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||||
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
|
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
|
||||||
return str(AcquisitionFeed(updated, id_, items, offsets,
|
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):
|
def opds_search(self, query=None, version=0, offset=0):
|
||||||
try:
|
try:
|
||||||
|
@ -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.
|
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.
|
||||||
|
|
||||||
|
108
src/calibre/manual/server.rst
Normal file
108
src/calibre/manual/server.rst
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
.. include:: global.rst
|
||||||
|
|
||||||
|
.. _servertutorial:
|
||||||
|
|
||||||
|
Integrating the |app| content server into other servers
|
||||||
|
==========================================================
|
||||||
|
|
||||||
|
Here, we will show you how to integrate the |app| content server into another server. The most common reason for this is to make use of SSL or more sophisticated authentication. There are two main techniques: Running the |app| content server as a standalone process and using a reverse proxy to connect it with your main server or running the content server in process in your main server with WSGI. The examples below are all for Apache 2.x on linux, but should be easily adaptable to other platforms.
|
||||||
|
|
||||||
|
.. contents:: Contents
|
||||||
|
:depth: 2
|
||||||
|
:local:
|
||||||
|
|
||||||
|
.. note:: This only applies to calibre releases >= 0.7.25
|
||||||
|
|
||||||
|
Using a reverse proxy
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements.
|
||||||
|
|
||||||
|
First start the |app| content server as shown below::
|
||||||
|
|
||||||
|
calibre-server --url-prefix /calibre --port 8080
|
||||||
|
|
||||||
|
Now suppose you are using Apache as your main server. First enable the proxy modules in apache, by adding the following to :file:`httpd.conf`::
|
||||||
|
|
||||||
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||||
|
|
||||||
|
The exact technique for enabling the proxy modules will vary depending on your Apache installation. Once you have the proxy modules enabled, add the following rules to httpd.conf (or if you are using virtual hosts to the conf file for the virtual host in question::
|
||||||
|
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy]
|
||||||
|
RewriteRule ^/calibre http://localhost:8080 [proxy]
|
||||||
|
|
||||||
|
That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server.
|
||||||
|
|
||||||
|
.. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive.
|
||||||
|
|
||||||
|
In process
|
||||||
|
------------
|
||||||
|
|
||||||
|
The calibre content server can be run directly, in process, inside a host server like Apache using the WSGI framework.
|
||||||
|
|
||||||
|
.. note:: For this to work, all the dependencies needed by calibre must be installed on your system. On linux, this can be achieved fairly easily by installing the distribution provided calibre package (provided it is up to date).
|
||||||
|
|
||||||
|
First, we have to create a WSGI *adapter* for the calibre content server. Here is a template you can use for the purpose. Replace the paths as directed in the comments
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# WSGI script file to run calibre content server as a WSGI app
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
|
||||||
|
# You can get the paths referenced here by running
|
||||||
|
# calibre-debug --paths
|
||||||
|
# on your server
|
||||||
|
|
||||||
|
# The first entry from CALIBRE_PYTHON_PATH
|
||||||
|
sys.path.insert(0, '/home/kovid/work/calibre/src')
|
||||||
|
|
||||||
|
# CALIBRE_RESOURCES_PATH
|
||||||
|
sys.resources_location = '/home/kovid/work/calibre/resources'
|
||||||
|
|
||||||
|
# CALIBRE_EXTENSIONS_PATH
|
||||||
|
sys.extensions_location = '/home/kovid/work/calibre/src/calibre/plugins'
|
||||||
|
|
||||||
|
# Path to directory containing calibre executables
|
||||||
|
sys.executables_location = '/usr/bin'
|
||||||
|
|
||||||
|
# Path to a directory for which the server has read/write permissions
|
||||||
|
# calibre config will be stored here
|
||||||
|
os.environ['CALIBRE_CONFIG_DIRECTORY'] = '/var/www/localhost/calibre-config'
|
||||||
|
|
||||||
|
del sys
|
||||||
|
del os
|
||||||
|
|
||||||
|
from calibre.library.server.main import create_wsgi_app
|
||||||
|
application = create_wsgi_app(
|
||||||
|
# The mount point of this WSGI application (i.e. the first argument to
|
||||||
|
# the WSGIScriptAlias directive). Set to empty string is mounted at /
|
||||||
|
prefix='/calibre',
|
||||||
|
|
||||||
|
# Path to the calibre library to be served
|
||||||
|
# The server process must have write permission for all files/dirs
|
||||||
|
# in this directory or BAD things will happen
|
||||||
|
path_to_library='/home/kovid/documents/demo library'
|
||||||
|
)
|
||||||
|
|
||||||
|
del create_wsgi_app
|
||||||
|
|
||||||
|
Save this adapter as :file:`calibre-wsgi-adpater.py` somewhere your server will have access to it.
|
||||||
|
|
||||||
|
Let's suppose that we want to use WSGI in Apache. First enable WSGI in Apache by adding the following to :file:`httpd.conf`::
|
||||||
|
|
||||||
|
LoadModule proxy_module modules/mod_wsgi.so
|
||||||
|
|
||||||
|
The exact technique for enabling the wsgi module will vary depending on your Apache installation. Once you have the proxy modules enabled, add the following rules to httpd.conf (or if you are using virtual hosts to the conf file for the virtual host in question::
|
||||||
|
|
||||||
|
WSGIScriptAlias /calibre /var/www/localhost/cgi-bin/calibre-wsgi-adapter.py
|
||||||
|
|
||||||
|
Change the path to :file:`calibre-wsgi-adapter.py` to wherever you saved it previously (make sure Apache has access to it).
|
||||||
|
|
||||||
|
That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server.
|
||||||
|
|
||||||
|
.. note:: For more help with using mod_wsgi in Apache, see `mod_wsgi <http://code.google.com/p/modwsgi/wiki/WhereToGetHelp>`_.
|
||||||
|
|
@ -16,4 +16,5 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
|
|||||||
template_lang
|
template_lang
|
||||||
regexp
|
regexp
|
||||||
portable
|
portable
|
||||||
|
server
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ being closed.
|
|||||||
"""
|
"""
|
||||||
import tempfile, os, atexit, binascii, cPickle
|
import tempfile, os, atexit, binascii, cPickle
|
||||||
|
|
||||||
from calibre import __version__, __appname__
|
from calibre.constants import __version__, __appname__
|
||||||
|
|
||||||
def cleanup(path):
|
def cleanup(path):
|
||||||
try:
|
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
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
@ -9,6 +9,7 @@ import os
|
|||||||
|
|
||||||
from calibre.utils.magick import Image, DrawingWand, create_canvas
|
from calibre.utils.magick import Image, DrawingWand, create_canvas
|
||||||
from calibre.constants import __appname__, __version__
|
from calibre.constants import __appname__, __version__
|
||||||
|
from calibre.utils.config import tweaks
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
def normalize_format_name(fmt):
|
def normalize_format_name(fmt):
|
||||||
@ -113,7 +114,9 @@ def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0,
|
|||||||
|
|
||||||
def create_text_wand(font_size, font_path=None):
|
def create_text_wand(font_size, font_path=None):
|
||||||
if font_path is None:
|
if font_path is None:
|
||||||
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
|
font_path = tweaks['generate_cover_title_font']
|
||||||
|
if font_path is None:
|
||||||
|
font_path = P('fonts/liberation/LiberationSerif-Bold.ttf')
|
||||||
ans = DrawingWand()
|
ans = DrawingWand()
|
||||||
ans.font = font_path
|
ans.font = font_path
|
||||||
ans.font_size = font_size
|
ans.font_size = font_size
|
||||||
@ -203,8 +206,11 @@ def create_cover_page(top_lines, logo_path, width=590, height=750,
|
|||||||
bottom += line.bottom_margin
|
bottom += line.bottom_margin
|
||||||
bottom -= top_lines[-1].bottom_margin
|
bottom -= top_lines[-1].bottom_margin
|
||||||
|
|
||||||
|
foot_font = tweaks['generate_cover_foot_font']
|
||||||
|
if not foot_font:
|
||||||
|
foot_font = P('fonts/liberation/LiberationMono-Regular.ttf')
|
||||||
vanity = create_text_arc(__appname__ + ' ' + __version__, 24,
|
vanity = create_text_arc(__appname__ + ' ' + __version__, 24,
|
||||||
font=P('fonts/liberation/LiberationMono-Regular.ttf'))
|
font=foot_font)
|
||||||
lwidth, lheight = vanity.size
|
lwidth, lheight = vanity.size
|
||||||
left = int(max(0, (width - lwidth)/2.))
|
left = int(max(0, (width - lwidth)/2.))
|
||||||
top = height - lheight - 10
|
top = height - lheight - 10
|
||||||
|
@ -583,7 +583,7 @@ class BasicNewsRecipe(Recipe):
|
|||||||
self.title = unicode(self.title, 'utf-8', 'replace')
|
self.title = unicode(self.title, 'utf-8', 'replace')
|
||||||
|
|
||||||
self.debug = options.verbose > 1
|
self.debug = options.verbose > 1
|
||||||
self.output_dir = os.getcwd()
|
self.output_dir = os.path.abspath(os.getcwdu())
|
||||||
self.verbose = options.verbose
|
self.verbose = options.verbose
|
||||||
self.test = options.test
|
self.test = options.test
|
||||||
self.username = options.username
|
self.username = options.username
|
||||||
@ -594,7 +594,6 @@ class BasicNewsRecipe(Recipe):
|
|||||||
if self.touchscreen:
|
if self.touchscreen:
|
||||||
self.template_css += self.output_profile.touchscreen_news_css
|
self.template_css += self.output_profile.touchscreen_news_css
|
||||||
|
|
||||||
self.output_dir = os.path.abspath(self.output_dir)
|
|
||||||
if options.test:
|
if options.test:
|
||||||
self.max_articles_per_feed = 2
|
self.max_articles_per_feed = 2
|
||||||
self.simultaneous_downloads = min(4, self.simultaneous_downloads)
|
self.simultaneous_downloads = min(4, self.simultaneous_downloads)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user