mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
[Sync] Sync with trunk r6726
This commit is contained in:
commit
f26affae90
@ -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" />
|
||||||
|
@ -109,7 +109,7 @@ function toplevel_layout() {
|
|||||||
var last = $(".toplevel li").last();
|
var last = $(".toplevel li").last();
|
||||||
var title = $('.toplevel h3').first();
|
var title = $('.toplevel h3').first();
|
||||||
var bottom = last.position().top + last.height() - title.position().top;
|
var bottom = last.position().top + last.height() - title.position().top;
|
||||||
$("#main").height(Math.max(200, bottom));
|
$("#main").height(Math.max(200, bottom+75));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toplevel() {
|
function toplevel() {
|
||||||
@ -156,6 +156,7 @@ function category() {
|
|||||||
if (href) {
|
if (href) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url:href,
|
url:href,
|
||||||
|
cache: false,
|
||||||
data:{'sort':cookie(sort_cookie_name)},
|
data:{'sort':cookie(sort_cookie_name)},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
this.children(".loaded").html(data);
|
this.children(".loaded").html(data);
|
||||||
@ -212,6 +213,7 @@ function load_page(elem) {
|
|||||||
url: href,
|
url: href,
|
||||||
context: elem,
|
context: elem,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
cache : false,
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
timeout: 600000, //milliseconds (10 minutes)
|
timeout: 600000, //milliseconds (10 minutes)
|
||||||
data: {'ids': ids},
|
data: {'ids': ids},
|
||||||
@ -255,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());
|
||||||
@ -263,6 +265,7 @@ function show_details(a_dom) {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
url: book.find('.details-href').attr('title'),
|
url: book.find('.details-href').attr('title'),
|
||||||
context: bd,
|
context: bd,
|
||||||
|
cache: false,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
timeout: 600000, //milliseconds (10 minutes)
|
timeout: 600000, //milliseconds (10 minutes)
|
||||||
error: function(xhr, stat, err) {
|
error: function(xhr, stat, err) {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
/* CSS for the mobile version of the content server webpage */
|
/* CSS for the mobile version of the content server webpage */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation table.buttons {
|
.navigation table.buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -53,6 +57,7 @@ div.navigation {
|
|||||||
}
|
}
|
||||||
#listing td {
|
#listing td {
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#listing td.thumbnail {
|
#listing td.thumbnail {
|
||||||
@ -73,6 +78,7 @@ div.navigation {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
@ -83,4 +89,17 @@ div.navigation {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-line {
|
||||||
|
font-size: larger;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-line {
|
||||||
|
margin-top: 0.75ex;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@ -106,7 +106,8 @@ title_sort_articles=r'^(A|The|An)\s+'
|
|||||||
auto_connect_to_folder = ''
|
auto_connect_to_folder = ''
|
||||||
|
|
||||||
|
|
||||||
# Specify renaming rules for sony collections. Collections on Sonys are named
|
# Specify renaming rules for sony collections. This tweak is only applicable if
|
||||||
|
# metadata management is set to automatic. Collections on Sonys are named
|
||||||
# depending upon whether the field is standard or custom. A collection derived
|
# depending upon whether the field is standard or custom. A collection derived
|
||||||
# from a standard field is named for the value in that field. For example, if
|
# from a standard field is named for the value in that field. For example, if
|
||||||
# the standard 'series' column contains the name 'Darkover', then the series
|
# the standard 'series' column contains the name 'Darkover', then the series
|
||||||
@ -137,6 +138,24 @@ auto_connect_to_folder = ''
|
|||||||
sony_collection_renaming_rules={}
|
sony_collection_renaming_rules={}
|
||||||
|
|
||||||
|
|
||||||
|
# Specify how sony collections are sorted. This tweak is only applicable if
|
||||||
|
# metadata management is set to automatic. You can indicate which metadata is to
|
||||||
|
# be used to sort on a collection-by-collection basis. The format of the tweak
|
||||||
|
# is a list of metadata fields from which collections are made, followed by the
|
||||||
|
# name of the metadata field containing the sort value.
|
||||||
|
# Example: The following indicates that collections built from pubdate and tags
|
||||||
|
# are to be sorted by the value in the custom column '#mydate', that collections
|
||||||
|
# built from 'series' are to be sorted by 'series_index', and that all other
|
||||||
|
# collections are to be sorted by title. If a collection metadata field is not
|
||||||
|
# named, then if it is a series- based collection it is sorted by series order,
|
||||||
|
# otherwise it is sorted by title order.
|
||||||
|
# [(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], 'title')]
|
||||||
|
# Note that the bracketing and parentheses are required. The syntax is
|
||||||
|
# [ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]
|
||||||
|
# Default: empty (no rules), so no collection attributes are named.
|
||||||
|
sony_collection_sorting_rules = []
|
||||||
|
|
||||||
|
|
||||||
# Create search terms to apply a query across several built-in search terms.
|
# Create search terms to apply a query across several built-in search terms.
|
||||||
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
|
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
|
||||||
# Example: create the term 'myseries' that when used as myseries:foo would
|
# Example: create the term 'myseries' that when used as myseries:foo would
|
||||||
@ -184,3 +203,11 @@ content_server_wont_display = ['']
|
|||||||
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
|
# 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/theecocolapse.png
Normal file
BIN
resources/images/news/theecocolapse.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@ -71,7 +71,9 @@ class TheAtlantic(BasicNewsRecipe):
|
|||||||
for poem in soup.findAll('div', attrs={'class':'poem'}):
|
for poem in soup.findAll('div', attrs={'class':'poem'}):
|
||||||
title = self.tag_to_string(poem.find('h4'))
|
title = self.tag_to_string(poem.find('h4'))
|
||||||
desc = self.tag_to_string(poem.find(attrs={'class':'author'}))
|
desc = self.tag_to_string(poem.find(attrs={'class':'author'}))
|
||||||
url = 'http://www.theatlantic.com'+poem.find('a')['href']
|
url = poem.find('a')['href']
|
||||||
|
if url.startswith('/'):
|
||||||
|
url = 'http://www.theatlantic.com' + url
|
||||||
self.log('\tFound article:', title, 'at', url)
|
self.log('\tFound article:', title, 'at', url)
|
||||||
self.log('\t\t', desc)
|
self.log('\t\t', desc)
|
||||||
poems.append({'title':title, 'url':url, 'description':desc,
|
poems.append({'title':title, 'url':url, 'description':desc,
|
||||||
@ -83,7 +85,9 @@ class TheAtlantic(BasicNewsRecipe):
|
|||||||
if div is not None:
|
if div is not None:
|
||||||
self.log('Found section: Advice')
|
self.log('Found section: Advice')
|
||||||
title = self.tag_to_string(div.find('h4'))
|
title = self.tag_to_string(div.find('h4'))
|
||||||
url = 'http://www.theatlantic.com'+div.find('a')['href']
|
url = div.find('a')['href']
|
||||||
|
if url.startswith('/'):
|
||||||
|
url = 'http://www.theatlantic.com' + url
|
||||||
desc = self.tag_to_string(div.find('p'))
|
desc = self.tag_to_string(div.find('p'))
|
||||||
self.log('\tFound article:', title, 'at', url)
|
self.log('\tFound article:', title, 'at', url)
|
||||||
self.log('\t\t', desc)
|
self.log('\t\t', desc)
|
||||||
|
@ -1,37 +1,37 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class AdvancedUserRecipe1286242553(BasicNewsRecipe):
|
class AdvancedUserRecipe1286242553(BasicNewsRecipe):
|
||||||
title = u'CACM'
|
title = u'CACM'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
feeds = [(u'CACM', u'http://cacm.acm.org/magazine.rss')]
|
feeds = [(u'CACM', u'http://cacm.acm.org/magazine.rss')]
|
||||||
language = 'en'
|
language = 'en'
|
||||||
__author__ = 'jonmisurda'
|
__author__ = 'jonmisurda'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':['FeatureBox', 'ArticleComments', 'SideColumn', \
|
dict(name='div', attrs={'class':['FeatureBox', 'ArticleComments', 'SideColumn', \
|
||||||
'LeftColumn', 'RightColumn', 'SiteSearch', 'MainNavBar','more', 'SubMenu', 'inner']})
|
'LeftColumn', 'RightColumn', 'SiteSearch', 'MainNavBar','more', 'SubMenu', 'inner']})
|
||||||
]
|
]
|
||||||
cover_url_pattern = 'http://cacm.acm.org/magazines/%d/%d'
|
cover_url_pattern = 'http://cacm.acm.org/magazines/%d/%d'
|
||||||
|
|
||||||
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:
|
||||||
br.open('https://cacm.acm.org/login')
|
br.open('https://cacm.acm.org/login')
|
||||||
br.select_form(nr=1)
|
br.select_form(nr=1)
|
||||||
br['current_member[user]'] = self.username
|
br['current_member[user]'] = self.username
|
||||||
br['current_member[passwd]'] = self.password
|
br['current_member[passwd]'] = self.password
|
||||||
br.submit()
|
br.submit()
|
||||||
return br
|
return br
|
||||||
|
|
||||||
def get_cover_url(self):
|
def get_cover_url(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
cover_url = None
|
cover_url = None
|
||||||
soup = self.index_to_soup(self.cover_url_pattern % (now.year, now.month))
|
soup = self.index_to_soup(self.cover_url_pattern % (now.year, now.month))
|
||||||
cover_item = soup.find('img',attrs={'alt':'magazine cover image'})
|
cover_item = soup.find('img',attrs={'alt':'magazine cover image'})
|
||||||
if cover_item:
|
if cover_item:
|
||||||
cover_url = cover_item['src']
|
cover_url = cover_item['src']
|
||||||
return cover_url
|
return cover_url
|
||||||
|
@ -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
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__author__ = 'Jordi Balcells, based on an earlier version by Lorenzo Vigentini & Kovid Goyal'
|
__author__ = 'Jordi Balcells, based on an earlier version by Lorenzo Vigentini & Kovid Goyal'
|
||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
description = 'Main daily newspaper from Spain - v1.03 (03, September 2010)'
|
description = 'Main daily newspaper from Spain - v1.04 (19, October 2010)'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -32,19 +32,16 @@ class ElPais(BasicNewsRecipe):
|
|||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
|
||||||
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia','cabecera_noticia_reportaje','cabecera_noticia_opinion','contenido_noticia','caja_despiece','presentacion']})]
|
keep_only_tags = [ dict(name='div', attrs={'class':['cabecera_noticia_reportaje estirar','cabecera_noticia_opinion estirar','cabecera_noticia estirar','contenido_noticia','caja_despiece']})]
|
||||||
|
|
||||||
extra_css = '''
|
|
||||||
p{style:normal size:12 serif}
|
|
||||||
|
|
||||||
'''
|
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:200%; font-weight: bolder; text-align: justify; } h2{ font-family: sans-serif; font-size:150%; font-weight: 500; text-align: justify } h3{ font-family: sans-serif; font-size:125%; font-weight: 500; text-align: justify } img{margin-bottom: 0.4em} '
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}),
|
dict(name='div', attrs={'class':['zona_superior','pie_enlaces_inferiores','contorno_f','ampliar']}),
|
||||||
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}),
|
dict(name='div', attrs={'class':['limpiar','mod_apoyo','borde_sup','votos estirar','info_complementa','info_relacionada','buscador_m','nav_ant_sig']}),
|
||||||
dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas','calendar']}),
|
dict(name='div', attrs={'id':['suscribirse suscrito','google_noticia','utilidades','coment','foros_not','pie','lomas','calendar']}),
|
||||||
dict(name='p', attrs={'class':'nav_meses'}),
|
dict(name='p', attrs={'class':'nav_meses'}),
|
||||||
dict(attrs={'class':['enlaces_m','miniaturas_m']})
|
dict(attrs={'class':['enlaces_m','miniaturas_m','nav_miniaturas_m']})
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
|
@ -4,7 +4,6 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
|||||||
foxnews.com
|
foxnews.com
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import re
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class FoxNews(BasicNewsRecipe):
|
class FoxNews(BasicNewsRecipe):
|
||||||
@ -21,11 +20,10 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
language = 'en'
|
language = 'en'
|
||||||
publication_type = 'newsportal'
|
publication_type = 'newsportal'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
extra_css = ' body{font-family: Arial,sans-serif } img{margin-bottom: 0.4em} .caption{font-size: x-small} '
|
extra_css = """
|
||||||
|
body{font-family: Arial,sans-serif }
|
||||||
preprocess_regexps = [
|
.caption{font-size: x-small}
|
||||||
(re.compile(r'</title>.*?</head>', re.DOTALL|re.IGNORECASE),lambda match: '</title></head>')
|
"""
|
||||||
]
|
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -34,27 +32,15 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_attributes = ['xmlns']
|
remove_attributes = ['xmlns','lang']
|
||||||
|
|
||||||
keep_only_tags = [
|
|
||||||
dict(name='div', attrs={'id' :['story','browse-story-content']})
|
|
||||||
,dict(name='div', attrs={'class':['posts articles','slideshow']})
|
|
||||||
,dict(name='h4' , attrs={'class':'storyDate'})
|
|
||||||
,dict(name='h1' , attrs={'xmlns:functx':'http://www.functx.com'})
|
|
||||||
,dict(name='div', attrs={'class':'authInfo'})
|
|
||||||
,dict(name='div', attrs={'id':'articleCont'})
|
|
||||||
]
|
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':['share-links','quigo quigo2','share-text','storyControls','socShare','btm-links']})
|
dict(name=['object','embed','link','script','iframe','meta','base'])
|
||||||
,dict(name='div', attrs={'id' :['otherMedia','loomia_display','img-all-path','story-vcmId','story-url','pane-browse-story-comments','story_related']})
|
,dict(attrs={'class':['user-control','url-description','ad-context']})
|
||||||
,dict(name='ul' , attrs={'class':['tools','tools alt','tools alt2','tabs']})
|
|
||||||
,dict(name='a' , attrs={'class':'join-discussion'})
|
|
||||||
,dict(name='ul' , attrs={'class':['tools','tools alt','tools alt2']})
|
|
||||||
,dict(name='p' , attrs={'class':'see_fullarchive'})
|
|
||||||
,dict(name=['object','embed','link','script'])
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
remove_tags_before=dict(name='h1')
|
||||||
|
remove_tags_after =dict(attrs={'class':'url-description'})
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Latest Headlines', u'http://feeds.foxnews.com/foxnews/latest' )
|
(u'Latest Headlines', u'http://feeds.foxnews.com/foxnews/latest' )
|
||||||
@ -67,8 +53,5 @@ class FoxNews(BasicNewsRecipe):
|
|||||||
,(u'Entertainment' , u'http://feeds.foxnews.com/foxnews/entertainment' )
|
,(u'Entertainment' , u'http://feeds.foxnews.com/foxnews/entertainment' )
|
||||||
]
|
]
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def print_version(self, url):
|
||||||
for item in soup.findAll(style=True):
|
return url + 'print'
|
||||||
del item['style']
|
|
||||||
return self.adeify_images(soup)
|
|
||||||
|
|
||||||
|
64
resources/recipes/ming_pao.recipe
Normal file
64
resources/recipes/ming_pao.recipe
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
cense__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Eddie Lau'
|
||||||
|
'''
|
||||||
|
modified from Singtao Toronto calibre recipe by rty
|
||||||
|
'''
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1278063072(BasicNewsRecipe):
|
||||||
|
title = 'Ming Pao - Hong Kong'
|
||||||
|
oldest_article = 1
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
__author__ = 'Eddie Lau'
|
||||||
|
description = 'Hong Kong Chinese Newspaper'
|
||||||
|
publisher = 'news.mingpao.com'
|
||||||
|
category = 'Chinese, News, Hong Kong'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'zh'
|
||||||
|
encoding = 'Big5-HKSCS'
|
||||||
|
recursions = 0
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
masthead_url = 'http://news.mingpao.com/image/portals_top_logo_news.gif'
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='h1'),
|
||||||
|
dict(attrs={'id':['newscontent01','newscontent02']})]
|
||||||
|
|
||||||
|
def get_fetchdate(self):
|
||||||
|
dt_utc = datetime.datetime.utcnow()
|
||||||
|
# convert UTC to local hk time
|
||||||
|
dt_local = dt_utc - datetime.timedelta(-8.0/24)
|
||||||
|
return dt_local.strftime("%Y%m%d")
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
feeds = []
|
||||||
|
dateStr = self.get_fetchdate()
|
||||||
|
for title, url in [(u'\u8981\u805e Headline', 'http://news.mingpao.com/' + dateStr + '/gaindex.htm'), (u'\u6559\u80b2 Education', 'http://news.mingpao.com/' + dateStr + '/gfindex.htm'), (u'\u6e2f\u805e Local', 'http://news.mingpao.com/' + dateStr + '/gbindex.htm'), (u'\u793e\u8a55\u2027\u7b46\u9663 Editorial', 'http://news.mingpao.com/' + dateStr + '/mrindex.htm'), (u'\u8ad6\u58c7 Forum', 'http://news.mingpao.com/' + dateStr + '/faindex.htm'), (u'\u4e2d\u570b China', 'http://news.mingpao.com/' + dateStr + '/caindex.htm'), (u'\u570b\u969b World', 'http://news.mingpao.com/' + dateStr + '/taindex.htm'), ('Tech News', 'http://news.mingpao.com/' + dateStr + '/naindex.htm'), (u'\u9ad4\u80b2 Sport', 'http://news.mingpao.com/' + dateStr + '/spindex.htm'), (u'\u526f\u520a Supplement', 'http://news.mingpao.com/' + dateStr + '/jaindex.htm'),]:
|
||||||
|
articles = self.parse_section(url)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def parse_section(self, url):
|
||||||
|
dateStr = self.get_fetchdate()
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
divs = soup.findAll(attrs={'class': ['bullet']})
|
||||||
|
current_articles = []
|
||||||
|
for i in divs:
|
||||||
|
a = i.find('a', href = True)
|
||||||
|
title = self.tag_to_string(a)
|
||||||
|
url = a.get('href', False)
|
||||||
|
url = 'http://news.mingpao.com/' + dateStr + '/' +url
|
||||||
|
current_articles.append({'title': title, 'url': url, 'description':''})
|
||||||
|
return current_articles
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll(width=True):
|
||||||
|
del item['width']
|
||||||
|
return soup
|
||||||
|
|
@ -8,11 +8,11 @@ import re
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class NewScientist(BasicNewsRecipe):
|
class NewScientist(BasicNewsRecipe):
|
||||||
title = 'New Scientist - Online News'
|
title = 'New Scientist - Online News w. subscription'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Science news and science articles from New Scientist.'
|
description = 'Science news and science articles from New Scientist.'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
publisher = 'New Scientist'
|
publisher = 'Reed Business Information Ltd.'
|
||||||
category = 'science news, science articles, science jobs, drugs, cancer, depression, computer software'
|
category = 'science news, science articles, science jobs, drugs, cancer, depression, computer software'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
@ -21,7 +21,12 @@ class NewScientist(BasicNewsRecipe):
|
|||||||
cover_url = 'http://www.newscientist.com/currentcover.jpg'
|
cover_url = 'http://www.newscientist.com/currentcover.jpg'
|
||||||
masthead_url = 'http://www.newscientist.com/img/misc/ns_logo.jpg'
|
masthead_url = 'http://www.newscientist.com/img/misc/ns_logo.jpg'
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
extra_css = ' body{font-family: Arial,sans-serif} img{margin-bottom: 0.8em} '
|
needs_subscription = 'optional'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Arial,sans-serif}
|
||||||
|
img{margin-bottom: 0.8em}
|
||||||
|
.quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -33,15 +38,27 @@ class NewScientist(BasicNewsRecipe):
|
|||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':['pgtop','maincol','blgmaincol','nsblgposts','hldgalcols']})]
|
keep_only_tags = [dict(name='div', attrs={'id':['pgtop','maincol','blgmaincol','nsblgposts','hldgalcols']})]
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
br.open('http://www.newscientist.com/')
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
br.open('https://www.newscientist.com/user/login?redirectURL=')
|
||||||
|
br.select_form(nr=2)
|
||||||
|
br['loginId' ] = self.username
|
||||||
|
br['password'] = self.password
|
||||||
|
br.submit()
|
||||||
|
return br
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]})
|
dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]})
|
||||||
,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial','sharebtns']})
|
,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial','sharebtns']})
|
||||||
,dict(name='p' , attrs={'class':['marker','infotext' ]})
|
,dict(name='p' , attrs={'class':['marker','infotext' ]})
|
||||||
,dict(name='meta' , attrs={'name' :'description' })
|
,dict(name='meta' , attrs={'name' :'description' })
|
||||||
,dict(name='a' , attrs={'rel' :'tag' })
|
,dict(name='a' , attrs={'rel' :'tag' })
|
||||||
|
,dict(name=['link','base','meta','iframe','object','embed'])
|
||||||
]
|
]
|
||||||
remove_tags_after = dict(attrs={'class':['nbpcopy','comments']})
|
remove_tags_after = dict(attrs={'class':['nbpcopy','comments']})
|
||||||
remove_attributes = ['height','width']
|
remove_attributes = ['height','width','lang']
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
|
(u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' )
|
||||||
@ -62,6 +79,8 @@ class NewScientist(BasicNewsRecipe):
|
|||||||
return url + '?full=true&print=true'
|
return url + '?full=true&print=true'
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(['quote','quotetext']):
|
||||||
|
item.name='p'
|
||||||
for tg in soup.findAll('a'):
|
for tg in soup.findAll('a'):
|
||||||
if tg.string == 'Home':
|
if tg.string == 'Home':
|
||||||
tg.parent.extract()
|
tg.parent.extract()
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
60
resources/recipes/stnn.recipe
Normal file
60
resources/recipes/stnn.recipe
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Larry Chan <larry1chan at gmail.com>'
|
||||||
|
'''
|
||||||
|
Singtao STNN
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class SingtaoSTNN(BasicNewsRecipe):
|
||||||
|
title = 'Singtao STNN'
|
||||||
|
__author__ = 'Larry Chan, larry1chan'
|
||||||
|
description = 'Chinese News'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
simultaneous_downloads = 5
|
||||||
|
no_stylesheets = True
|
||||||
|
#delay = 1
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'gb2312'
|
||||||
|
publisher = 'Singtao STNN'
|
||||||
|
category = 'news, China, world'
|
||||||
|
language = 'zh'
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
|
||||||
|
masthead_url = 'http://www.stnn.cc/images/0806/logo_080728.gif'
|
||||||
|
conversion_options = {
|
||||||
|
'comments' : description
|
||||||
|
,'tags' : category
|
||||||
|
,'language' : language
|
||||||
|
,'publisher' : publisher
|
||||||
|
,'linearize_tables': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div', attrs={'class':['page_box']})
|
||||||
|
remove_tags_after = dict(name='div', attrs={'class':['pagelist']})
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['font_title clearfix']}),
|
||||||
|
dict(name='div', attrs={'id':['content_zoom']})
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_attributes = ['width','height','href']
|
||||||
|
|
||||||
|
# for a full list of rss check out [url]http://www.stnn.cc/rss/[/url]
|
||||||
|
|
||||||
|
feeds = [ (u'Headline News', u'http://www.stnn.cc/rss/news/index.xml'),
|
||||||
|
(u'Breaking News', u'http://www.stnn.cc/rss/tufa/index.xml'),
|
||||||
|
(u'Finance', u'http://www.stnn.cc/rss/fin/index.xml'),
|
||||||
|
(u'Entertainment', u'http://www.stnn.cc/rss/ent/index.xml'),
|
||||||
|
(u'International', u'http://www.stnn.cc/rss/guoji/index.xml'),
|
||||||
|
(u'China', u'http://www.stnn.cc/rss/china/index.xml'),
|
||||||
|
(u'Opnion', u'http://www.stnn.cc/rss/fin_op/index.xml'),
|
||||||
|
(u'Blog', u'http://blog.stnn.cc/uploadfile/rssblogtypehotlog.xml'),
|
||||||
|
(u'Hong Kong', u'http://www.stnn.cc/rss/hongkong/index.xml')
|
||||||
|
|
||||||
|
]
|
||||||
|
|
46
resources/recipes/theecocolapse.recipe
Normal file
46
resources/recipes/theecocolapse.recipe
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
theeconomiccollapseblog.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
class TheEconomicCollapse(BasicNewsRecipe):
|
||||||
|
title = 'The Economic Collapse'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Are You Prepared For The Coming Economic Collapse And The Next Great Depression?'
|
||||||
|
publisher = 'The Economic Collapse'
|
||||||
|
category = 'news, politics, USA, economy'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Tahoma,Arial,sans-serif }
|
||||||
|
img{margin-bottom: 0.4em}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(attrs={'class':'sociable'})
|
||||||
|
,dict(name=['iframe','object','embed','meta','link','base'])
|
||||||
|
]
|
||||||
|
remove_attributes=['lang','onclick','width','height']
|
||||||
|
keep_only_tags=[dict(attrs={'class':['post-headline','post-bodycopy clearfix','']})]
|
||||||
|
|
||||||
|
feeds = [(u'Posts', u'http://theeconomiccollapseblog.com/feed')]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
@ -19,20 +19,22 @@ class TheEconomicTimes(BasicNewsRecipe):
|
|||||||
simultaneous_downloads = 1
|
simultaneous_downloads = 1
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
language = 'en_IN'
|
language = 'en_IN'
|
||||||
publication_type = 'newspaper'
|
publication_type = 'newspaper'
|
||||||
masthead_url = 'http://economictimes.indiatimes.com/photo/2676871.cms'
|
masthead_url = 'http://economictimes.indiatimes.com/photo/2676871.cms'
|
||||||
extra_css = """ body{font-family: Arial,Helvetica,sans-serif}
|
extra_css = """
|
||||||
.heading1{font-size: xx-large; font-weight: bold} """
|
body{font-family: Arial,Helvetica,sans-serif}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
, 'tags' : category
|
, 'tags' : category
|
||||||
, 'publisher' : publisher
|
, 'publisher' : publisher
|
||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(attrs={'class':['heading1','headingnext','Normal']})]
|
keep_only_tags = [dict(attrs={'class':'printdiv'})]
|
||||||
remove_tags = [dict(name=['object','link','embed','iframe','base','table','meta'])]
|
remove_tags = [dict(name=['object','link','embed','iframe','base','table','meta'])]
|
||||||
|
remove_attributes = ['name']
|
||||||
|
|
||||||
feeds = [(u'All articles', u'http://economictimes.indiatimes.com/rssfeedsdefault.cms')]
|
feeds = [(u'All articles', u'http://economictimes.indiatimes.com/rssfeedsdefault.cms')]
|
||||||
|
|
||||||
@ -48,5 +50,5 @@ class TheEconomicTimes(BasicNewsRecipe):
|
|||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
for item in soup.findAll(style=True):
|
for item in soup.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
return self.adeify_images(soup)
|
return self.adeify_images(soup)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -294,3 +294,8 @@ class OutputFormatPlugin(Plugin):
|
|||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_periodical(self):
|
||||||
|
return self.oeb.metadata.publication_type and \
|
||||||
|
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:')
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__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):
|
||||||
|
|
||||||
@ -583,7 +591,8 @@ class KindleDXOutput(OutputProfile):
|
|||||||
# Screen size is a best guess
|
# Screen size is a best guess
|
||||||
screen_size = (744, 1022)
|
screen_size = (744, 1022)
|
||||||
dpi = 150.0
|
dpi = 150.0
|
||||||
comic_screen_size = (741, 1022)
|
comic_screen_size = (771, 1116)
|
||||||
|
#comic_screen_size = (741, 1022)
|
||||||
supports_mobi_indexing = True
|
supports_mobi_indexing = True
|
||||||
periodical_date_in_title = False
|
periodical_date_in_title = False
|
||||||
mobi_ems_per_blockquote = 2.0
|
mobi_ems_per_blockquote = 2.0
|
||||||
@ -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],
|
||||||
|
@ -42,7 +42,7 @@ class CYBOOK(USBMS):
|
|||||||
DELETE_EXTS = ['.mbp', '.dat', '.bin', '_6090.t2b', '.thn']
|
DELETE_EXTS = ['.mbp', '.dat', '.bin', '_6090.t2b', '.thn']
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
coverdata = getattr(metadata, 'thumbnail', None)
|
coverdata = getattr(metadata, 'thumbnail', None)
|
||||||
if coverdata and coverdata[2]:
|
if coverdata and coverdata[2]:
|
||||||
coverdata = coverdata[2]
|
coverdata = coverdata[2]
|
||||||
|
@ -77,7 +77,7 @@ class ALEX(N516):
|
|||||||
name = os.path.splitext(os.path.basename(file_abspath))[0] + '.png'
|
name = os.path.splitext(os.path.basename(file_abspath))[0] + '.png'
|
||||||
return os.path.join(base, 'covers', name)
|
return os.path.join(base, 'covers', name)
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
from calibre.ebooks import calibre_cover
|
from calibre.ebooks import calibre_cover
|
||||||
from calibre.utils.magick.draw import thumbnail
|
from calibre.utils.magick.draw import thumbnail
|
||||||
coverdata = getattr(metadata, 'thumbnail', None)
|
coverdata = getattr(metadata, 'thumbnail', None)
|
||||||
@ -129,7 +129,7 @@ class AZBOOKA(ALEX):
|
|||||||
def can_handle(self, device_info, debug=False):
|
def can_handle(self, device_info, debug=False):
|
||||||
return not is_alex(device_info)
|
return not is_alex(device_info)
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class EB511(USBMS):
|
class EB511(USBMS):
|
||||||
|
@ -22,7 +22,9 @@ class KOBO(USBMS):
|
|||||||
gui_name = 'Kobo Reader'
|
gui_name = 'Kobo Reader'
|
||||||
description = _('Communicate with the Kobo Reader')
|
description = _('Communicate with the Kobo Reader')
|
||||||
author = 'Timothy Legge and Kovid Goyal'
|
author = 'Timothy Legge and Kovid Goyal'
|
||||||
version = (1, 0, 6)
|
version = (1, 0, 7)
|
||||||
|
|
||||||
|
dbversion = 0
|
||||||
|
|
||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
|
||||||
@ -92,7 +94,7 @@ class KOBO(USBMS):
|
|||||||
if lpath.startswith(os.sep):
|
if lpath.startswith(os.sep):
|
||||||
lpath = lpath[len(os.sep):]
|
lpath = lpath[len(os.sep):]
|
||||||
lpath = lpath.replace('\\', '/')
|
lpath = lpath.replace('\\', '/')
|
||||||
# print "LPATH: " + lpath
|
# debug_print("LPATH: ", lpath, " - Title: " , title)
|
||||||
|
|
||||||
playlist_map = {}
|
playlist_map = {}
|
||||||
|
|
||||||
@ -112,7 +114,7 @@ class KOBO(USBMS):
|
|||||||
#print "Image name Normalized: " + imagename
|
#print "Image name Normalized: " + imagename
|
||||||
if imagename is not None:
|
if imagename is not None:
|
||||||
bl[idx].thumbnail = ImageWrapper(imagename)
|
bl[idx].thumbnail = ImageWrapper(imagename)
|
||||||
if ContentType != '6':
|
if (ContentType != '6'and self.dbversion < 8) or (self.dbversion >= 8):
|
||||||
if self.update_metadata_item(bl[idx]):
|
if self.update_metadata_item(bl[idx]):
|
||||||
# print 'update_metadata_item returned true'
|
# print 'update_metadata_item returned true'
|
||||||
changed = True
|
changed = True
|
||||||
@ -120,10 +122,16 @@ class KOBO(USBMS):
|
|||||||
playlist_map[lpath] not in bl[idx].device_collections:
|
playlist_map[lpath] not in bl[idx].device_collections:
|
||||||
bl[idx].device_collections.append(playlist_map[lpath])
|
bl[idx].device_collections.append(playlist_map[lpath])
|
||||||
else:
|
else:
|
||||||
if ContentType == '6':
|
if ContentType == '6' and self.dbversion < 8:
|
||||||
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
|
||||||
else:
|
else:
|
||||||
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
try:
|
||||||
|
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
|
||||||
|
except:
|
||||||
|
debug_print("prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors, \
|
||||||
|
"mime: ", mime, "date: ", date, "ContentType: ", ContentType, "ImageID: ", ImageID)
|
||||||
|
raise
|
||||||
|
|
||||||
# print 'Update booklist'
|
# print 'Update booklist'
|
||||||
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else []
|
book.device_collections = [playlist_map[lpath]] if lpath in playlist_map else []
|
||||||
|
|
||||||
@ -143,6 +151,13 @@ class KOBO(USBMS):
|
|||||||
# numrows = row[0]
|
# numrows = row[0]
|
||||||
#cursor.close()
|
#cursor.close()
|
||||||
|
|
||||||
|
# Determine the database version
|
||||||
|
# 4 - Bluetooth Kobo Rev 2 (1.4)
|
||||||
|
# 8 - WIFI KOBO Rev 1
|
||||||
|
cursor.execute('select version from dbversion')
|
||||||
|
result = cursor.fetchone()
|
||||||
|
self.dbversion = result[0]
|
||||||
|
|
||||||
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
|
||||||
'ImageID, ReadStatus from content where BookID is Null'
|
'ImageID, ReadStatus from content where BookID is Null'
|
||||||
|
|
||||||
@ -153,7 +168,8 @@ class KOBO(USBMS):
|
|||||||
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
|
||||||
|
|
||||||
path = self.path_from_contentid(row[3], row[5], oncard)
|
path = self.path_from_contentid(row[3], row[5], oncard)
|
||||||
mime = mime_type_ext(path_to_ext(row[3]))
|
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
|
||||||
|
# debug_print("mime:", mime)
|
||||||
|
|
||||||
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
|
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
|
||||||
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7])
|
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7])
|
||||||
@ -206,7 +222,7 @@ class KOBO(USBMS):
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
if ContentType == 6:
|
if ContentType == 6 and self.dbversion < 8:
|
||||||
# Delete the shortcover_pages first
|
# Delete the shortcover_pages first
|
||||||
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
|
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
|
||||||
|
|
||||||
@ -249,7 +265,7 @@ class KOBO(USBMS):
|
|||||||
path = self.normalize_path(path)
|
path = self.normalize_path(path)
|
||||||
# print "Delete file normalized path: " + path
|
# print "Delete file normalized path: " + path
|
||||||
extension = os.path.splitext(path)[1]
|
extension = os.path.splitext(path)[1]
|
||||||
ContentType = self.get_content_type_from_extension(extension)
|
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(path)
|
||||||
|
|
||||||
ContentID = self.contentid_from_path(path, ContentType)
|
ContentID = self.contentid_from_path(path, ContentType)
|
||||||
|
|
||||||
@ -332,9 +348,14 @@ class KOBO(USBMS):
|
|||||||
|
|
||||||
def contentid_from_path(self, path, ContentType):
|
def contentid_from_path(self, path, ContentType):
|
||||||
if ContentType == 6:
|
if ContentType == 6:
|
||||||
ContentID = os.path.splitext(path)[0]
|
if self.dbversion < 8:
|
||||||
# Remove the prefix on the file. it could be either
|
ContentID = os.path.splitext(path)[0]
|
||||||
ContentID = ContentID.replace(self._main_prefix, '')
|
# Remove the prefix on the file. it could be either
|
||||||
|
ContentID = ContentID.replace(self._main_prefix, '')
|
||||||
|
else:
|
||||||
|
ContentID = path
|
||||||
|
ContentID = ContentID.replace(self._main_prefix + '.kobo/kepub/', '')
|
||||||
|
|
||||||
if self._card_a_prefix is not None:
|
if self._card_a_prefix is not None:
|
||||||
ContentID = ContentID.replace(self._card_a_prefix, '')
|
ContentID = ContentID.replace(self._card_a_prefix, '')
|
||||||
elif ContentType == 999: # HTML Files
|
elif ContentType == 999: # HTML Files
|
||||||
@ -350,6 +371,13 @@ class KOBO(USBMS):
|
|||||||
ContentID = ContentID.replace("\\", '/')
|
ContentID = ContentID.replace("\\", '/')
|
||||||
return ContentID
|
return ContentID
|
||||||
|
|
||||||
|
def get_content_type_from_path(self, path):
|
||||||
|
# Strictly speaking the ContentType could be 6 or 10
|
||||||
|
# however newspapers have the same storage format
|
||||||
|
if path.find('kepub') >= 0:
|
||||||
|
ContentType = 6
|
||||||
|
return ContentType
|
||||||
|
|
||||||
def get_content_type_from_extension(self, extension):
|
def get_content_type_from_extension(self, extension):
|
||||||
if extension == '.kobo':
|
if extension == '.kobo':
|
||||||
# Kobo books do not have book files. They do have some images though
|
# Kobo books do not have book files. They do have some images though
|
||||||
@ -369,19 +397,22 @@ class KOBO(USBMS):
|
|||||||
print 'path from_contentid cardb'
|
print 'path from_contentid cardb'
|
||||||
elif oncard == 'carda':
|
elif oncard == 'carda':
|
||||||
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
|
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
|
||||||
# print "SD Card: " + filename
|
# print "SD Card: " + path
|
||||||
else:
|
else:
|
||||||
if ContentType == "6":
|
if ContentType == "6" and self.dbversion < 8:
|
||||||
# This is a hack as the kobo files do not exist
|
# This is a hack as the kobo files do not exist
|
||||||
# but the path is required to make a unique id
|
# but the path is required to make a unique id
|
||||||
# for calibre's reference
|
# for calibre's reference
|
||||||
path = self._main_prefix + path + '.kobo'
|
path = self._main_prefix + path + '.kobo'
|
||||||
# print "Path: " + path
|
# print "Path: " + path
|
||||||
|
elif (ContentType == "6" or ContentType == "10") and self.dbversion >= 8:
|
||||||
|
path = self._main_prefix + '.kobo/kepub/' + path
|
||||||
|
# print "Internal: " + path
|
||||||
else:
|
else:
|
||||||
# if path.startswith("file:///mnt/onboard/"):
|
# if path.startswith("file:///mnt/onboard/"):
|
||||||
path = path.replace("file:///mnt/onboard/", self._main_prefix)
|
path = path.replace("file:///mnt/onboard/", self._main_prefix)
|
||||||
path = path.replace("/mnt/onboard/", self._main_prefix)
|
path = path.replace("/mnt/onboard/", self._main_prefix)
|
||||||
# print "Internal: " + filename
|
# print "Internal: " + path
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@ -469,7 +500,7 @@ class KOBO(USBMS):
|
|||||||
book.device_collections = ['Im_Reading']
|
book.device_collections = ['Im_Reading']
|
||||||
|
|
||||||
extension = os.path.splitext(book.path)[1]
|
extension = os.path.splitext(book.path)[1]
|
||||||
ContentType = self.get_content_type_from_extension(extension)
|
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
|
||||||
|
|
||||||
ContentID = self.contentid_from_path(book.path, ContentType)
|
ContentID = self.contentid_from_path(book.path, ContentType)
|
||||||
datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
||||||
@ -505,7 +536,7 @@ class KOBO(USBMS):
|
|||||||
book.device_collections = ['Read']
|
book.device_collections = ['Read']
|
||||||
|
|
||||||
extension = os.path.splitext(book.path)[1]
|
extension = os.path.splitext(book.path)[1]
|
||||||
ContentType = self.get_content_type_from_extension(extension)
|
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
|
||||||
|
|
||||||
ContentID = self.contentid_from_path(book.path, ContentType)
|
ContentID = self.contentid_from_path(book.path, ContentType)
|
||||||
# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())
|
||||||
|
@ -102,7 +102,7 @@ class PDNOVEL(USBMS):
|
|||||||
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
|
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
|
||||||
|
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
coverdata = getattr(metadata, 'thumbnail', None)
|
coverdata = getattr(metadata, 'thumbnail', None)
|
||||||
if coverdata and coverdata[2]:
|
if coverdata and coverdata[2]:
|
||||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ class NOOK(USBMS):
|
|||||||
DELETE_EXTS = ['.jpg']
|
DELETE_EXTS = ['.jpg']
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
Image, ImageDraw
|
Image, ImageDraw
|
||||||
|
@ -2,5 +2,11 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
MEDIA_XML = 'database/cache/media.xml'
|
MEDIA_XML = 'database/cache/media.xml'
|
||||||
|
MEDIA_EXT = 'database/cache/cacheExt.xml'
|
||||||
|
|
||||||
CACHE_XML = 'Sony Reader/database/cache.xml'
|
CACHE_XML = 'Sony Reader/database/cache.xml'
|
||||||
|
CACHE_EXT = 'Sony Reader/database/cacheExt.xml'
|
||||||
|
|
||||||
|
MEDIA_THUMBNAIL = 'database/thumbnail'
|
||||||
|
CACHE_THUMBNAIL = 'Sony Reader/database/thumbnail'
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ Device driver for the SONY devices
|
|||||||
import os, time, re
|
import os, time, re
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS, debug_print
|
from calibre.devices.usbms.driver import USBMS, debug_print
|
||||||
from calibre.devices.prs505 import MEDIA_XML
|
from calibre.devices.prs505 import MEDIA_XML, MEDIA_EXT, CACHE_XML, CACHE_EXT, \
|
||||||
from calibre.devices.prs505 import CACHE_XML
|
MEDIA_THUMBNAIL, CACHE_THUMBNAIL
|
||||||
from calibre.devices.prs505.sony_cache import XMLCache
|
from calibre.devices.prs505.sony_cache import XMLCache
|
||||||
from calibre import __appname__
|
from calibre import __appname__, prints
|
||||||
from calibre.devices.usbms.books import CollectionsBookList
|
from calibre.devices.usbms.books import CollectionsBookList
|
||||||
|
|
||||||
class PRS505(USBMS):
|
class PRS505(USBMS):
|
||||||
@ -66,6 +66,8 @@ class PRS505(USBMS):
|
|||||||
plugboard = None
|
plugboard = None
|
||||||
plugboard_func = None
|
plugboard_func = None
|
||||||
|
|
||||||
|
THUMBNAIL_HEIGHT = 200
|
||||||
|
|
||||||
def windows_filter_pnp_id(self, pnp_id):
|
def windows_filter_pnp_id(self, pnp_id):
|
||||||
return '_LAUNCHER' in pnp_id
|
return '_LAUNCHER' in pnp_id
|
||||||
|
|
||||||
@ -116,20 +118,21 @@ class PRS505(USBMS):
|
|||||||
return fname
|
return fname
|
||||||
|
|
||||||
def initialize_XML_cache(self):
|
def initialize_XML_cache(self):
|
||||||
paths, prefixes = {}, {}
|
paths, prefixes, ext_paths = {}, {}, {}
|
||||||
for prefix, path, source_id in [
|
for prefix, path, ext_path, source_id in [
|
||||||
('main', MEDIA_XML, 0),
|
('main', MEDIA_XML, MEDIA_EXT, 0),
|
||||||
('card_a', CACHE_XML, 1),
|
('card_a', CACHE_XML, CACHE_EXT, 1),
|
||||||
('card_b', CACHE_XML, 2)
|
('card_b', CACHE_XML, CACHE_EXT, 2)
|
||||||
]:
|
]:
|
||||||
prefix = getattr(self, '_%s_prefix'%prefix)
|
prefix = getattr(self, '_%s_prefix'%prefix)
|
||||||
if prefix is not None and os.path.exists(prefix):
|
if prefix is not None and os.path.exists(prefix):
|
||||||
paths[source_id] = os.path.join(prefix, *(path.split('/')))
|
paths[source_id] = os.path.join(prefix, *(path.split('/')))
|
||||||
|
ext_paths[source_id] = os.path.join(prefix, *(ext_path.split('/')))
|
||||||
prefixes[source_id] = prefix
|
prefixes[source_id] = prefix
|
||||||
d = os.path.dirname(paths[source_id])
|
d = os.path.dirname(paths[source_id])
|
||||||
if not os.path.exists(d):
|
if not os.path.exists(d):
|
||||||
os.makedirs(d)
|
os.makedirs(d)
|
||||||
return XMLCache(paths, prefixes, self.settings().use_author_sort)
|
return XMLCache(paths, ext_paths, prefixes, self.settings().use_author_sort)
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
debug_print('PRS505: starting fetching books for card', oncard)
|
debug_print('PRS505: starting fetching books for card', oncard)
|
||||||
@ -174,3 +177,31 @@ class PRS505(USBMS):
|
|||||||
def set_plugboards(self, plugboards, pb_func):
|
def set_plugboards(self, plugboards, pb_func):
|
||||||
self.plugboards = plugboards
|
self.plugboards = plugboards
|
||||||
self.plugboard_func = pb_func
|
self.plugboard_func = pb_func
|
||||||
|
|
||||||
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
|
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||||
|
path = path.replace('/', os.sep)
|
||||||
|
is_main = path.startswith(self._main_prefix)
|
||||||
|
thumbnail_dir = MEDIA_THUMBNAIL if is_main else CACHE_THUMBNAIL
|
||||||
|
prefix = None
|
||||||
|
if is_main:
|
||||||
|
prefix = self._main_prefix
|
||||||
|
else:
|
||||||
|
if self._card_a_prefix and \
|
||||||
|
path.startswith(self._card_a_prefix):
|
||||||
|
prefix = self._card_a_prefix
|
||||||
|
elif self._card_b_prefix and \
|
||||||
|
path.startswith(self._card_b_prefix):
|
||||||
|
prefix = self._card_b_prefix
|
||||||
|
if prefix is None:
|
||||||
|
prints('WARNING: Failed to find prefix for:', filepath)
|
||||||
|
return
|
||||||
|
thumbnail_dir = os.path.join(prefix, *thumbnail_dir.split('/'))
|
||||||
|
|
||||||
|
relpath = os.path.relpath(filepath, prefix)
|
||||||
|
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
|
||||||
|
if not os.path.exists(thumbnail_dir):
|
||||||
|
os.makedirs(thumbnail_dir)
|
||||||
|
with open(os.path.join(thumbnail_dir, 'main_thumbnail.jpg'), 'wb') as f:
|
||||||
|
f.write(metadata.thumbnail[-1])
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import os, time
|
|||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from calibre import prints, guess_type, isbytestring
|
from calibre import prints, guess_type, isbytestring
|
||||||
from calibre.devices.errors import DeviceError
|
from calibre.devices.errors import DeviceError
|
||||||
@ -18,6 +19,20 @@ from calibre.ebooks.chardet import xml_to_unicode
|
|||||||
from calibre.ebooks.metadata import authors_to_string, title_sort, \
|
from calibre.ebooks.metadata import authors_to_string, title_sort, \
|
||||||
authors_to_sort_string
|
authors_to_sort_string
|
||||||
|
|
||||||
|
'''
|
||||||
|
cahceExt.xml
|
||||||
|
|
||||||
|
Periodical identifier sample from a PRS-650:
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<cacheExt xmlns="http://www.sony.com/xmlns/product/prs/device/1">
|
||||||
|
<text conformsTo="http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0" periodicalName="The Atlantic" description="Current affairs and politics focussed on the US" publicationDate="Tue, 19 Oct 2010 00:00:00 GMT" path="database/media/books/calibre/Atlantic [Mon, 18 Oct 2010], The - calibre_1701.epub">
|
||||||
|
<thumbnail width="167" height="217">main_thumbnail.jpg</thumbnail>
|
||||||
|
</text>
|
||||||
|
</cacheExt>
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
# Utility functions {{{
|
# Utility functions {{{
|
||||||
EMPTY_CARD_CACHE = '''\
|
EMPTY_CARD_CACHE = '''\
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@ -25,6 +40,12 @@ EMPTY_CARD_CACHE = '''\
|
|||||||
</cache>
|
</cache>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
EMPTY_EXT_CACHE = '''\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<cacheExt xmlns="http://www.sony.com/xmlns/product/prs/device/1">
|
||||||
|
</cacheExt>
|
||||||
|
'''
|
||||||
|
|
||||||
MIME_MAP = {
|
MIME_MAP = {
|
||||||
"lrf" : "application/x-sony-bbeb",
|
"lrf" : "application/x-sony-bbeb",
|
||||||
'lrx' : 'application/x-sony-bbeb',
|
'lrx' : 'application/x-sony-bbeb',
|
||||||
@ -63,7 +84,7 @@ def uuid():
|
|||||||
|
|
||||||
class XMLCache(object):
|
class XMLCache(object):
|
||||||
|
|
||||||
def __init__(self, paths, prefixes, use_author_sort):
|
def __init__(self, paths, ext_paths, prefixes, use_author_sort):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
debug_print('Building XMLCache...', paths)
|
debug_print('Building XMLCache...', paths)
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
@ -76,8 +97,8 @@ class XMLCache(object):
|
|||||||
for source_id, path in paths.items():
|
for source_id, path in paths.items():
|
||||||
if source_id == 0:
|
if source_id == 0:
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise DeviceError('The SONY XML cache media.xml does not exist. Try'
|
raise DeviceError(('The SONY XML cache %r does not exist. Try'
|
||||||
' disconnecting and reconnecting your reader.')
|
' disconnecting and reconnecting your reader.')%repr(path))
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
else:
|
else:
|
||||||
@ -85,14 +106,34 @@ class XMLCache(object):
|
|||||||
if os.access(path, os.R_OK):
|
if os.access(path, os.R_OK):
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
|
|
||||||
self.roots[source_id] = etree.fromstring(xml_to_unicode(
|
self.roots[source_id] = etree.fromstring(xml_to_unicode(
|
||||||
raw, strip_encoding_pats=True, assume_utf8=True,
|
raw, strip_encoding_pats=True, assume_utf8=True,
|
||||||
verbose=DEBUG)[0],
|
verbose=DEBUG)[0],
|
||||||
parser=parser)
|
parser=parser)
|
||||||
if self.roots[source_id] is None:
|
if self.roots[source_id] is None:
|
||||||
raise Exception(('The SONY database at %s is corrupted. Try '
|
raise Exception(('The SONY database at %r is corrupted. Try '
|
||||||
' disconnecting and reconnecting your reader.')%path)
|
' disconnecting and reconnecting your reader.')%path)
|
||||||
|
|
||||||
|
self.ext_paths, self.ext_roots = {}, {}
|
||||||
|
for source_id, path in ext_paths.items():
|
||||||
|
if not os.path.exists(path):
|
||||||
|
try:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(EMPTY_EXT_CACHE)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if os.access(path, os.W_OK):
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
self.ext_roots[source_id] = etree.fromstring(
|
||||||
|
xml_to_unicode(f.read(),
|
||||||
|
strip_encoding_pats=True, assume_utf8=True,
|
||||||
|
verbose=DEBUG)[0], parser=parser)
|
||||||
|
self.ext_paths[source_id] = path
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
||||||
@ -352,12 +393,18 @@ class XMLCache(object):
|
|||||||
debug_print('Updating XML Cache:', i)
|
debug_print('Updating XML Cache:', i)
|
||||||
root = self.record_roots[i]
|
root = self.record_roots[i]
|
||||||
lpath_map = self.build_lpath_map(root)
|
lpath_map = self.build_lpath_map(root)
|
||||||
|
ext_root = self.ext_roots[i] if i in self.ext_roots else None
|
||||||
|
ext_lpath_map = None
|
||||||
|
if ext_root is not None:
|
||||||
|
ext_lpath_map = self.build_lpath_map(ext_root)
|
||||||
gtz_count = ltz_count = 0
|
gtz_count = ltz_count = 0
|
||||||
use_tz_var = False
|
use_tz_var = False
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
||||||
record = lpath_map.get(book.lpath, None)
|
record = lpath_map.get(book.lpath, None)
|
||||||
|
created = False
|
||||||
if record is None:
|
if record is None:
|
||||||
|
created = True
|
||||||
record = self.create_text_record(root, i, book.lpath)
|
record = self.create_text_record(root, i, book.lpath)
|
||||||
if plugboard is not None:
|
if plugboard is not None:
|
||||||
newmi = book.deepcopy_metadata()
|
newmi = book.deepcopy_metadata()
|
||||||
@ -373,6 +420,13 @@ class XMLCache(object):
|
|||||||
if book.device_collections is None:
|
if book.device_collections is None:
|
||||||
book.device_collections = []
|
book.device_collections = []
|
||||||
book.device_collections = playlist_map.get(book.lpath, [])
|
book.device_collections = playlist_map.get(book.lpath, [])
|
||||||
|
|
||||||
|
if created and ext_root is not None and \
|
||||||
|
ext_lpath_map.get(book.lpath, None) is None:
|
||||||
|
ext_record = self.create_ext_text_record(ext_root, i,
|
||||||
|
book.lpath, book.thumbnail)
|
||||||
|
self.periodicalize_book(book, ext_record)
|
||||||
|
|
||||||
debug_print('Timezone votes: %d GMT, %d LTZ, use_tz_var=%s'%
|
debug_print('Timezone votes: %d GMT, %d LTZ, use_tz_var=%s'%
|
||||||
(gtz_count, ltz_count, use_tz_var))
|
(gtz_count, ltz_count, use_tz_var))
|
||||||
self.update_playlists(i, root, booklist, collections_attributes)
|
self.update_playlists(i, root, booklist, collections_attributes)
|
||||||
@ -386,6 +440,47 @@ class XMLCache(object):
|
|||||||
self.fix_ids()
|
self.fix_ids()
|
||||||
debug_print('Finished update')
|
debug_print('Finished update')
|
||||||
|
|
||||||
|
def is_sony_periodical(self, book):
|
||||||
|
if _('News') not in book.tags:
|
||||||
|
return False
|
||||||
|
if not book.lpath.lower().endswith('.epub'):
|
||||||
|
return False
|
||||||
|
if book.pubdate.date() < date(2010, 10, 17):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def periodicalize_book(self, book, record):
|
||||||
|
if not self.is_sony_periodical(book):
|
||||||
|
return
|
||||||
|
record.set('conformsTo',
|
||||||
|
"http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0")
|
||||||
|
|
||||||
|
record.set('description', '')
|
||||||
|
|
||||||
|
name = None
|
||||||
|
if '[' in book.title:
|
||||||
|
name = book.title.split('[')[0].strip()
|
||||||
|
if len(name) < 4:
|
||||||
|
name = None
|
||||||
|
if not name:
|
||||||
|
try:
|
||||||
|
name = [t for t in book.tags if t != _('News')][0]
|
||||||
|
except:
|
||||||
|
name = None
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = book.title
|
||||||
|
|
||||||
|
record.set('periodicalName', name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pubdate = strftime(book.pubdate.utctimetuple(),
|
||||||
|
zone=lambda x : x)
|
||||||
|
record.set('publicationDate', pubdate)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def rebuild_collections(self, booklist, bl_index):
|
def rebuild_collections(self, booklist, bl_index):
|
||||||
if bl_index not in self.record_roots:
|
if bl_index not in self.record_roots:
|
||||||
return
|
return
|
||||||
@ -472,6 +567,28 @@ class XMLCache(object):
|
|||||||
root.append(ans)
|
root.append(ans)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def create_ext_text_record(self, root, bl_id, lpath, thumbnail):
|
||||||
|
namespace = root.nsmap[None]
|
||||||
|
attrib = { 'path': lpath }
|
||||||
|
ans = root.makeelement('{%s}text'%namespace, attrib=attrib,
|
||||||
|
nsmap=root.nsmap)
|
||||||
|
ans.tail = '\n'
|
||||||
|
if len(root) > 0:
|
||||||
|
root[-1].tail = '\n\t'
|
||||||
|
else:
|
||||||
|
root.text = '\n\t'
|
||||||
|
root.append(ans)
|
||||||
|
if thumbnail and thumbnail[-1]:
|
||||||
|
ans.text = '\n' + '\t\t'
|
||||||
|
t = root.makeelement('{%s}thumbnail'%namespace,
|
||||||
|
attrib={'width':str(thumbnail[0]), 'height':str(thumbnail[1])},
|
||||||
|
nsmap=root.nsmap)
|
||||||
|
t.text = 'main_thumbnail.jpg'
|
||||||
|
ans.append(t)
|
||||||
|
t.tail = '\n\t'
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def update_text_record(self, record, book, path, bl_index,
|
def update_text_record(self, record, book, path, bl_index,
|
||||||
gtz_count, ltz_count, use_tz_var):
|
gtz_count, ltz_count, use_tz_var):
|
||||||
'''
|
'''
|
||||||
@ -589,6 +706,18 @@ class XMLCache(object):
|
|||||||
'<?xml version="1.0" encoding="UTF-8"?>')
|
'<?xml version="1.0" encoding="UTF-8"?>')
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
|
||||||
|
for i, path in self.ext_paths.items():
|
||||||
|
try:
|
||||||
|
raw = etree.tostring(self.ext_roots[i], encoding='UTF-8',
|
||||||
|
xml_declaration=True)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
raw = raw.replace("<?xml version='1.0' encoding='UTF-8'?>",
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>')
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(raw)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Utility methods {{{
|
# Utility methods {{{
|
||||||
|
@ -5,8 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import dbus
|
import dbus, os
|
||||||
import os
|
|
||||||
|
|
||||||
def node_mountpoint(node):
|
def node_mountpoint(node):
|
||||||
|
|
||||||
@ -56,15 +55,6 @@ class UDisks(object):
|
|||||||
parent = device_node_path
|
parent = device_node_path
|
||||||
while parent[-1] in '0123456789':
|
while parent[-1] in '0123456789':
|
||||||
parent = parent[:-1]
|
parent = parent[:-1]
|
||||||
devices = [str(x) for x in self.main.EnumerateDeviceFiles()]
|
|
||||||
for d in devices:
|
|
||||||
if d.startswith(parent) and d != parent:
|
|
||||||
try:
|
|
||||||
self.unmount(d)
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
print 'Failed to unmount:', d
|
|
||||||
traceback.print_exc()
|
|
||||||
d = self.device(parent)
|
d = self.device(parent)
|
||||||
d.DriveEject([])
|
d.DriveEject([])
|
||||||
|
|
||||||
@ -76,13 +66,19 @@ def eject(node_path):
|
|||||||
u = UDisks()
|
u = UDisks()
|
||||||
u.eject(node_path)
|
u.eject(node_path)
|
||||||
|
|
||||||
|
def umount(node_path):
|
||||||
|
u = UDisks()
|
||||||
|
u.unmount(node_path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
dev = sys.argv[1]
|
dev = sys.argv[1]
|
||||||
print 'Testing with node', dev
|
print 'Testing with node', dev
|
||||||
u = UDisks()
|
u = UDisks()
|
||||||
print 'Mounted at:', u.mount(dev)
|
print 'Mounted at:', u.mount(dev)
|
||||||
print 'Ejecting'
|
print 'Unmounting'
|
||||||
|
u.unmount(dev)
|
||||||
|
print 'Ejecting:'
|
||||||
u.eject(dev)
|
u.eject(dev)
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,6 +99,13 @@ class CollectionsBookList(BookList):
|
|||||||
def supports_collections(self):
|
def supports_collections(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def in_category_sort_rules(self, attr):
|
||||||
|
sorts = tweaks['sony_collection_sorting_rules']
|
||||||
|
for attrs,sortattr in sorts:
|
||||||
|
if attr in attrs or '*' in attrs:
|
||||||
|
return sortattr
|
||||||
|
return None
|
||||||
|
|
||||||
def compute_category_name(self, attr, category, field_meta):
|
def compute_category_name(self, attr, category, field_meta):
|
||||||
renames = tweaks['sony_collection_renaming_rules']
|
renames = tweaks['sony_collection_renaming_rules']
|
||||||
attr_name = renames.get(attr, None)
|
attr_name = renames.get(attr, None)
|
||||||
@ -116,6 +123,7 @@ class CollectionsBookList(BookList):
|
|||||||
from calibre.devices.usbms.driver import debug_print
|
from calibre.devices.usbms.driver import debug_print
|
||||||
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
|
debug_print('Starting get_collections:', prefs['manage_device_metadata'])
|
||||||
debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
|
debug_print('Renaming rules:', tweaks['sony_collection_renaming_rules'])
|
||||||
|
debug_print('Sorting rules:', tweaks['sony_collection_sorting_rules'])
|
||||||
|
|
||||||
# Complexity: we can use renaming rules only when using automatic
|
# Complexity: we can use renaming rules only when using automatic
|
||||||
# management. Otherwise we don't always have the metadata to make the
|
# management. Otherwise we don't always have the metadata to make the
|
||||||
@ -171,6 +179,7 @@ class CollectionsBookList(BookList):
|
|||||||
else:
|
else:
|
||||||
val = [val]
|
val = [val]
|
||||||
|
|
||||||
|
sort_attr = self.in_category_sort_rules(attr)
|
||||||
for category in val:
|
for category in val:
|
||||||
is_series = False
|
is_series = False
|
||||||
if doing_dc:
|
if doing_dc:
|
||||||
@ -199,22 +208,41 @@ class CollectionsBookList(BookList):
|
|||||||
|
|
||||||
if cat_name not in collections:
|
if cat_name not in collections:
|
||||||
collections[cat_name] = {}
|
collections[cat_name] = {}
|
||||||
if is_series:
|
if use_renaming_rules and sort_attr:
|
||||||
|
sort_val = book.get(sort_attr, None)
|
||||||
|
collections[cat_name][lpath] = \
|
||||||
|
(book, sort_val, book.get('title_sort', 'zzzz'))
|
||||||
|
elif is_series:
|
||||||
if doing_dc:
|
if doing_dc:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = \
|
||||||
(book, book.get('series_index', sys.maxint))
|
(book, book.get('series_index', sys.maxint), '')
|
||||||
else:
|
else:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = \
|
||||||
(book, book.get(attr+'_index', sys.maxint))
|
(book, book.get(attr+'_index', sys.maxint), '')
|
||||||
else:
|
else:
|
||||||
if lpath not in collections[cat_name]:
|
if lpath not in collections[cat_name]:
|
||||||
collections[cat_name][lpath] = \
|
collections[cat_name][lpath] = \
|
||||||
(book, book.get('title_sort', 'zzzz'))
|
(book, book.get('title_sort', 'zzzz'), '')
|
||||||
# Sort collections
|
# Sort collections
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
|
def none_cmp(xx, yy):
|
||||||
|
x = xx[1]
|
||||||
|
y = yy[1]
|
||||||
|
if x is None and y is None:
|
||||||
|
return cmp(xx[2], yy[2])
|
||||||
|
if x is None:
|
||||||
|
return 1
|
||||||
|
if y is None:
|
||||||
|
return -1
|
||||||
|
c = cmp(x, y)
|
||||||
|
if c != 0:
|
||||||
|
return c
|
||||||
|
return cmp(xx[2], yy[2])
|
||||||
|
|
||||||
for category, lpaths in collections.items():
|
for category, lpaths in collections.items():
|
||||||
books = lpaths.values()
|
books = lpaths.values()
|
||||||
books.sort(cmp=lambda x,y:cmp(x[1], y[1]))
|
books.sort(cmp=none_cmp)
|
||||||
result[category] = [x[0] for x in books]
|
result[category] = [x[0] for x in books]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -523,7 +523,8 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
devnodes.append(node)
|
devnodes.append(node)
|
||||||
|
|
||||||
devnodes += list(repeat(None, 3))
|
devnodes += list(repeat(None, 3))
|
||||||
ans = tuple(['/dev/'+x if ok.get(x, False) else None for x in devnodes[:3]])
|
ans = ['/dev/'+x if ok.get(x, False) else None for x in devnodes[:3]]
|
||||||
|
ans.sort(key=lambda x: x[5:] if x else 'zzzzz')
|
||||||
return self.linux_swap_drives(ans)
|
return self.linux_swap_drives(ans)
|
||||||
|
|
||||||
def linux_swap_drives(self, drives):
|
def linux_swap_drives(self, drives):
|
||||||
@ -732,24 +733,36 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def eject_linux(self):
|
def eject_linux(self):
|
||||||
try:
|
from calibre.devices.udisks import eject, umount
|
||||||
from calibre.devices.udisks import eject
|
drives = [d for d in self.find_device_nodes() if d]
|
||||||
return eject(self._linux_main_device_node)
|
for d in drives:
|
||||||
except:
|
try:
|
||||||
pass
|
umount(d)
|
||||||
drives = self.find_device_nodes()
|
except:
|
||||||
|
pass
|
||||||
|
failures = False
|
||||||
|
for d in drives:
|
||||||
|
try:
|
||||||
|
eject(d)
|
||||||
|
except Exception, e:
|
||||||
|
print 'Udisks eject call for:', d, 'failed:'
|
||||||
|
print '\t', e
|
||||||
|
failures = True
|
||||||
|
|
||||||
|
if not failures:
|
||||||
|
return
|
||||||
|
|
||||||
for drive in drives:
|
for drive in drives:
|
||||||
if drive:
|
cmd = 'calibre-mount-helper'
|
||||||
cmd = 'calibre-mount-helper'
|
if getattr(sys, 'frozen_path', False):
|
||||||
if getattr(sys, 'frozen_path', False):
|
cmd = os.path.join(sys.frozen_path, cmd)
|
||||||
cmd = os.path.join(sys.frozen_path, cmd)
|
cmd = [cmd, 'eject']
|
||||||
cmd = [cmd, 'eject']
|
mp = getattr(self, "_linux_mount_map", {}).get(drive,
|
||||||
mp = getattr(self, "_linux_mount_map", {}).get(drive,
|
'dummy/')[:-1]
|
||||||
'dummy/')[:-1]
|
try:
|
||||||
try:
|
subprocess.Popen(cmd + [drive, mp]).wait()
|
||||||
subprocess.Popen(cmd + [drive, mp]).wait()
|
except:
|
||||||
except:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
def eject(self):
|
def eject(self):
|
||||||
if islinux:
|
if islinux:
|
||||||
|
@ -186,7 +186,8 @@ class USBMS(CLI, Device):
|
|||||||
self.put_file(infile, filepath, replace_file=True)
|
self.put_file(infile, filepath, replace_file=True)
|
||||||
try:
|
try:
|
||||||
self.upload_cover(os.path.dirname(filepath),
|
self.upload_cover(os.path.dirname(filepath),
|
||||||
os.path.splitext(os.path.basename(filepath))[0], mdata)
|
os.path.splitext(os.path.basename(filepath))[0],
|
||||||
|
mdata, filepath)
|
||||||
except: # Failure to upload cover is not catastrophic
|
except: # Failure to upload cover is not catastrophic
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -197,14 +198,15 @@ class USBMS(CLI, Device):
|
|||||||
debug_print('USBMS: finished uploading %d books'%(len(files)))
|
debug_print('USBMS: finished uploading %d books'%(len(files)))
|
||||||
return zip(paths, cycle([on_card]))
|
return zip(paths, cycle([on_card]))
|
||||||
|
|
||||||
def upload_cover(self, path, filename, metadata):
|
def upload_cover(self, path, filename, metadata, filepath):
|
||||||
'''
|
'''
|
||||||
Upload book cover to the device. Default implementation does nothing.
|
Upload book cover to the device. Default implementation does nothing.
|
||||||
|
|
||||||
:param path: the full path were the associated book is located.
|
:param path: The full path to the directory where the associated book is located.
|
||||||
:param filename: the name of the book file without the extension.
|
:param filename: The name of the book file without the extension.
|
||||||
:param metadata: metadata belonging to the book. Use metadata.thumbnail
|
:param metadata: metadata belonging to the book. Use metadata.thumbnail
|
||||||
for cover
|
for cover
|
||||||
|
:param filepath: The full path to the ebook file
|
||||||
|
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
@ -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)
|
||||||
|
@ -15,22 +15,30 @@ def rules(stylesheets):
|
|||||||
if r.type == r.STYLE_RULE:
|
if r.type == r.STYLE_RULE:
|
||||||
yield r
|
yield r
|
||||||
|
|
||||||
def initialize_container(path_to_container, opf_name='metadata.opf'):
|
def initialize_container(path_to_container, opf_name='metadata.opf',
|
||||||
|
extra_entries=[]):
|
||||||
'''
|
'''
|
||||||
Create an empty EPUB document, with a default skeleton.
|
Create an empty EPUB document, with a default skeleton.
|
||||||
'''
|
'''
|
||||||
CONTAINER='''\
|
rootfiles = ''
|
||||||
|
for path, mimetype, _ in extra_entries:
|
||||||
|
rootfiles += u'<rootfile full-path="{0}" media-type="{1}"/>'.format(
|
||||||
|
path, mimetype)
|
||||||
|
CONTAINER = u'''\
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||||
<rootfiles>
|
<rootfiles>
|
||||||
<rootfile full-path="%s" media-type="application/oebps-package+xml"/>
|
<rootfile full-path="{0}" media-type="application/oebps-package+xml"/>
|
||||||
|
{extra_entries}
|
||||||
</rootfiles>
|
</rootfiles>
|
||||||
</container>
|
</container>
|
||||||
'''%opf_name
|
'''.format(opf_name, extra_entries=rootfiles).encode('utf-8')
|
||||||
zf = ZipFile(path_to_container, 'w')
|
zf = ZipFile(path_to_container, 'w')
|
||||||
zf.writestr('mimetype', 'application/epub+zip', compression=ZIP_STORED)
|
zf.writestr('mimetype', 'application/epub+zip', compression=ZIP_STORED)
|
||||||
zf.writestr('META-INF/', '', 0700)
|
zf.writestr('META-INF/', '', 0700)
|
||||||
zf.writestr('META-INF/container.xml', CONTAINER)
|
zf.writestr('META-INF/container.xml', CONTAINER)
|
||||||
|
for path, _, data in extra_entries:
|
||||||
|
zf.writestr(path, data)
|
||||||
return zf
|
return zf
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,6 +108,27 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
open('calibre_raster_cover.jpg', 'wb').write(
|
open('calibre_raster_cover.jpg', 'wb').write(
|
||||||
renderer)
|
renderer)
|
||||||
|
|
||||||
|
def find_opf(self):
|
||||||
|
def attr(n, attr):
|
||||||
|
for k, v in n.attrib.items():
|
||||||
|
if k.endswith(attr):
|
||||||
|
return v
|
||||||
|
try:
|
||||||
|
with open('META-INF/container.xml') as f:
|
||||||
|
root = etree.fromstring(f.read())
|
||||||
|
for r in root.xpath('//*[local-name()="rootfile"]'):
|
||||||
|
if attr(r, 'media-type') != "application/oebps-package+xml":
|
||||||
|
continue
|
||||||
|
path = attr(r, 'full-path')
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
path = os.path.join(os.getcwdu(), *path.split('/'))
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def convert(self, stream, options, file_ext, log, accelerators):
|
def convert(self, stream, options, file_ext, log, accelerators):
|
||||||
from calibre.utils.zipfile import ZipFile
|
from calibre.utils.zipfile import ZipFile
|
||||||
from calibre import walk
|
from calibre import walk
|
||||||
@ -116,12 +137,13 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
zf = ZipFile(stream)
|
zf = ZipFile(stream)
|
||||||
zf.extractall(os.getcwd())
|
zf.extractall(os.getcwd())
|
||||||
encfile = os.path.abspath(os.path.join('META-INF', 'encryption.xml'))
|
encfile = os.path.abspath(os.path.join('META-INF', 'encryption.xml'))
|
||||||
opf = None
|
opf = self.find_opf()
|
||||||
for f in walk(u'.'):
|
if opf is None:
|
||||||
if f.lower().endswith('.opf') and '__MACOSX' not in f and \
|
for f in walk(u'.'):
|
||||||
not os.path.basename(f).startswith('.'):
|
if f.lower().endswith('.opf') and '__MACOSX' not in f and \
|
||||||
opf = os.path.abspath(f)
|
not os.path.basename(f).startswith('.'):
|
||||||
break
|
opf = os.path.abspath(f)
|
||||||
|
break
|
||||||
path = getattr(stream, 'name', 'stream')
|
path = getattr(stream, 'name', 'stream')
|
||||||
|
|
||||||
if opf is None:
|
if opf is None:
|
||||||
|
@ -106,6 +106,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def workaround_webkit_quirks(self): # {{{
|
def workaround_webkit_quirks(self): # {{{
|
||||||
from calibre.ebooks.oeb.base import XPath
|
from calibre.ebooks.oeb.base import XPath
|
||||||
for x in self.oeb.spine:
|
for x in self.oeb.spine:
|
||||||
@ -183,6 +184,12 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
|
|
||||||
with TemporaryDirectory('_epub_output') as tdir:
|
with TemporaryDirectory('_epub_output') as tdir:
|
||||||
from calibre.customize.ui import plugin_for_output_format
|
from calibre.customize.ui import plugin_for_output_format
|
||||||
|
metadata_xml = None
|
||||||
|
extra_entries = []
|
||||||
|
if self.is_periodical:
|
||||||
|
from calibre.ebooks.epub.periodical import sony_metadata
|
||||||
|
metadata_xml, atom_xml = sony_metadata(oeb)
|
||||||
|
extra_entries = [('atom.xml', 'application/atom+xml', atom_xml)]
|
||||||
oeb_output = plugin_for_output_format('oeb')
|
oeb_output = plugin_for_output_format('oeb')
|
||||||
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
|
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
|
||||||
opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
|
opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0]
|
||||||
@ -194,10 +201,14 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid)
|
encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid)
|
||||||
|
|
||||||
from calibre.ebooks.epub import initialize_container
|
from calibre.ebooks.epub import initialize_container
|
||||||
epub = initialize_container(output_path, os.path.basename(opf))
|
epub = initialize_container(output_path, os.path.basename(opf),
|
||||||
|
extra_entries=extra_entries)
|
||||||
epub.add_dir(tdir)
|
epub.add_dir(tdir)
|
||||||
if encryption is not None:
|
if encryption is not None:
|
||||||
epub.writestr('META-INF/encryption.xml', encryption)
|
epub.writestr('META-INF/encryption.xml', encryption)
|
||||||
|
if metadata_xml is not None:
|
||||||
|
epub.writestr('META-INF/metadata.xml',
|
||||||
|
metadata_xml.encode('utf-8'))
|
||||||
if opts.extract_to is not None:
|
if opts.extract_to is not None:
|
||||||
if os.path.exists(opts.extract_to):
|
if os.path.exists(opts.extract_to):
|
||||||
shutil.rmtree(opts.extract_to)
|
shutil.rmtree(opts.extract_to)
|
||||||
|
173
src/calibre/ebooks/epub/periodical.py
Normal file
173
src/calibre/ebooks/epub/periodical.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from calibre.constants import __appname__, __version__
|
||||||
|
from calibre import strftime, prepare_string_for_xml as xml
|
||||||
|
|
||||||
|
SONY_METADATA = u'''\
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:prs="http://xmlns.sony.net/e-book/prs/">
|
||||||
|
<rdf:Description rdf:about="">
|
||||||
|
<dc:title>{title}</dc:title>
|
||||||
|
<dc:publisher>{publisher}</dc:publisher>
|
||||||
|
<dcterms:alternative>{short_title}</dcterms:alternative>
|
||||||
|
<dcterms:issued>{issue_date}</dcterms:issued>
|
||||||
|
<dc:language>{language}</dc:language>
|
||||||
|
<dcterms:conformsTo rdf:resource="http://xmlns.sony.net/e-book/prs/periodicals/1.0/newspaper/1.0"/>
|
||||||
|
<dcterms:type rdf:resource="http://xmlns.sony.net/e-book/prs/datatype/newspaper"/>
|
||||||
|
<dcterms:type rdf:resource="http://xmlns.sony.net/e-book/prs/datatype/periodical"/>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
'''
|
||||||
|
|
||||||
|
SONY_ATOM = u'''\
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:prs="http://xmlns.sony.net/e-book/prs/"
|
||||||
|
xmlns:media="http://video.search.yahoo.com/mrss"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
|
||||||
|
<title>{short_title}</title>
|
||||||
|
<updated>{updated}</updated>
|
||||||
|
<id>{id}</id>
|
||||||
|
{entries}
|
||||||
|
</feed>
|
||||||
|
'''
|
||||||
|
|
||||||
|
SONY_ATOM_SECTION = u'''\
|
||||||
|
<entry rdf:ID="{title}">
|
||||||
|
<title>{title}</title>
|
||||||
|
<link href="{href}"/>
|
||||||
|
<id>{id}</id>
|
||||||
|
<updated>{updated}</updated>
|
||||||
|
<summary>{desc}</summary>
|
||||||
|
<category term="{short_title}/{title}"
|
||||||
|
scheme="http://xmlns.sony.net/e-book/terms/" label="{title}"/>
|
||||||
|
<dc:type xsi:type="prs:datatype">newspaper/section</dc:type>
|
||||||
|
<dcterms:isReferencedBy rdf:resource=""/>
|
||||||
|
</entry>
|
||||||
|
'''
|
||||||
|
|
||||||
|
SONY_ATOM_ENTRY = u'''\
|
||||||
|
<entry>
|
||||||
|
<title>{title}</title>
|
||||||
|
<author><name>{author}</name></author>
|
||||||
|
<link href="{href}"/>
|
||||||
|
<id>{id}</id>
|
||||||
|
<updated>{updated}</updated>
|
||||||
|
<summary>{desc}</summary>
|
||||||
|
<category term="{short_title}/{section_title}"
|
||||||
|
scheme="http://xmlns.sony.net/e-book/terms/" label="{section_title}"/>
|
||||||
|
<dcterms:extent xsi:type="prs:word-count">{word_count}</dcterms:extent>
|
||||||
|
<dc:type xsi:type="prs:datatype">newspaper/article</dc:type>
|
||||||
|
<dcterms:isReferencedBy rdf:resource="#{section_title}"/>
|
||||||
|
</entry>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def sony_metadata(oeb):
|
||||||
|
m = oeb.metadata
|
||||||
|
title = short_title = unicode(m.title[0])
|
||||||
|
publisher = __appname__ + ' ' + __version__
|
||||||
|
try:
|
||||||
|
pt = unicode(oeb.metadata.publication_type[0])
|
||||||
|
short_title = u':'.join(pt.split(':')[2:])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
date = unicode(m.date[0]).split('T')[0]
|
||||||
|
except:
|
||||||
|
date = strftime('%Y-%m-%d')
|
||||||
|
try:
|
||||||
|
language = unicode(m.language[0]).replace('_', '-')
|
||||||
|
except:
|
||||||
|
language = 'en'
|
||||||
|
short_title = xml(short_title, True)
|
||||||
|
|
||||||
|
metadata = SONY_METADATA.format(title=xml(title),
|
||||||
|
short_title=short_title,
|
||||||
|
publisher=xml(publisher), issue_date=xml(date),
|
||||||
|
language=xml(language))
|
||||||
|
|
||||||
|
updated = strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
|
def cal_id(x):
|
||||||
|
for k, v in x.attrib.items():
|
||||||
|
if k.endswith('scheme') and v == 'uuid':
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
base_id = unicode(list(filter(cal_id, m.identifier))[0])
|
||||||
|
except:
|
||||||
|
base_id = str(uuid4())
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
seen_titles = set([])
|
||||||
|
for i, section in enumerate(oeb.toc):
|
||||||
|
if not section.href:
|
||||||
|
continue
|
||||||
|
secid = 'section%d'%i
|
||||||
|
sectitle = section.title
|
||||||
|
if not sectitle:
|
||||||
|
sectitle = _('Unknown')
|
||||||
|
d = 1
|
||||||
|
bsectitle = sectitle
|
||||||
|
while sectitle in seen_titles:
|
||||||
|
sectitle = bsectitle + ' ' + str(d)
|
||||||
|
d += 1
|
||||||
|
seen_titles.add(sectitle)
|
||||||
|
sectitle = xml(sectitle, True)
|
||||||
|
secdesc = section.description
|
||||||
|
if not secdesc:
|
||||||
|
secdesc = ''
|
||||||
|
secdesc = xml(secdesc)
|
||||||
|
entries.append(SONY_ATOM_SECTION.format(title=sectitle,
|
||||||
|
href=section.href, id=xml(base_id)+'/'+secid,
|
||||||
|
short_title=short_title, desc=secdesc, updated=updated))
|
||||||
|
|
||||||
|
for j, article in enumerate(section):
|
||||||
|
if not article.href:
|
||||||
|
continue
|
||||||
|
atitle = article.title
|
||||||
|
btitle = atitle
|
||||||
|
d = 1
|
||||||
|
while atitle in seen_titles:
|
||||||
|
atitle = btitle + ' ' + str(d)
|
||||||
|
d += 1
|
||||||
|
|
||||||
|
auth = article.author if article.author else ''
|
||||||
|
desc = section.description
|
||||||
|
if not desc:
|
||||||
|
desc = ''
|
||||||
|
aid = 'article%d'%j
|
||||||
|
|
||||||
|
entries.append(SONY_ATOM_ENTRY.format(
|
||||||
|
title=xml(atitle),
|
||||||
|
author=xml(auth),
|
||||||
|
updated=updated,
|
||||||
|
desc=desc,
|
||||||
|
short_title=short_title,
|
||||||
|
section_title=sectitle,
|
||||||
|
href=article.href,
|
||||||
|
word_count=str(1),
|
||||||
|
id=xml(base_id)+'/'+secid+'/'+aid
|
||||||
|
))
|
||||||
|
|
||||||
|
atom = SONY_ATOM.format(short_title=short_title,
|
||||||
|
entries='\n\n'.join(entries), updated=updated,
|
||||||
|
id=xml(base_id)).encode('utf-8')
|
||||||
|
|
||||||
|
return metadata, atom
|
||||||
|
|
@ -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__':
|
||||||
|
@ -43,7 +43,7 @@ class SafeFormat(TemplateFormatter):
|
|||||||
b = self.book.get_user_metadata(key, False)
|
b = self.book.get_user_metadata(key, False)
|
||||||
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0:
|
||||||
v = ''
|
v = ''
|
||||||
elif b and b['datatype'] == 'float' and b.get(key, 0.0) == 0.0:
|
elif b and b['datatype'] == 'float' and self.book.get(key, 0.0) == 0.0:
|
||||||
v = ''
|
v = ''
|
||||||
else:
|
else:
|
||||||
ign, v = self.book.format_field(key.lower(), series_with_index=False)
|
ign, v = self.book.format_field(key.lower(), series_with_index=False)
|
||||||
@ -501,7 +501,7 @@ class Metadata(object):
|
|||||||
if key.startswith('#') and key.endswith('_index'):
|
if key.startswith('#') and key.endswith('_index'):
|
||||||
tkey = key[:-6] # strip the _index
|
tkey = key[:-6] # strip the _index
|
||||||
cmeta = self.get_user_metadata(tkey, make_copy=False)
|
cmeta = self.get_user_metadata(tkey, make_copy=False)
|
||||||
if cmeta['datatype'] == 'series':
|
if cmeta and cmeta['datatype'] == 'series':
|
||||||
if self.get(tkey):
|
if self.get(tkey):
|
||||||
res = self.get_extra(tkey)
|
res = self.get_extra(tkey)
|
||||||
return (unicode(cmeta['name']+'_index'),
|
return (unicode(cmeta['name']+'_index'),
|
||||||
|
@ -382,11 +382,13 @@ class Guide(ResourceCollection): # {{{
|
|||||||
|
|
||||||
class MetadataField(object):
|
class MetadataField(object):
|
||||||
|
|
||||||
def __init__(self, name, is_dc=True, formatter=None, none_is=None):
|
def __init__(self, name, is_dc=True, formatter=None, none_is=None,
|
||||||
|
renderer=lambda x: unicode(x)):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.is_dc = is_dc
|
self.is_dc = is_dc
|
||||||
self.formatter = formatter
|
self.formatter = formatter
|
||||||
self.none_is = none_is
|
self.none_is = none_is
|
||||||
|
self.renderer = renderer
|
||||||
|
|
||||||
def __real_get__(self, obj, type=None):
|
def __real_get__(self, obj, type=None):
|
||||||
ans = obj.get_metadata_element(self.name)
|
ans = obj.get_metadata_element(self.name)
|
||||||
@ -418,7 +420,7 @@ class MetadataField(object):
|
|||||||
return
|
return
|
||||||
if elem is None:
|
if elem is None:
|
||||||
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
|
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
|
||||||
obj.set_text(elem, unicode(val))
|
obj.set_text(elem, self.renderer(val))
|
||||||
|
|
||||||
|
|
||||||
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
|
def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)):
|
||||||
@ -489,10 +491,11 @@ class OPF(object): # {{{
|
|||||||
series = MetadataField('series', is_dc=False)
|
series = MetadataField('series', is_dc=False)
|
||||||
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
|
series_index = MetadataField('series_index', is_dc=False, formatter=float, none_is=1)
|
||||||
rating = MetadataField('rating', is_dc=False, formatter=int)
|
rating = MetadataField('rating', is_dc=False, formatter=int)
|
||||||
pubdate = MetadataField('date', formatter=parse_date)
|
pubdate = MetadataField('date', formatter=parse_date,
|
||||||
|
renderer=isoformat)
|
||||||
publication_type = MetadataField('publication_type', is_dc=False)
|
publication_type = MetadataField('publication_type', is_dc=False)
|
||||||
timestamp = MetadataField('timestamp', is_dc=False,
|
timestamp = MetadataField('timestamp', is_dc=False,
|
||||||
formatter=parse_date)
|
formatter=parse_date, renderer=isoformat)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True,
|
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True,
|
||||||
@ -826,11 +829,10 @@ class OPF(object): # {{{
|
|||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
matches = self.isbn_path(self.metadata)
|
matches = self.isbn_path(self.metadata)
|
||||||
if val is None:
|
if not val:
|
||||||
if matches:
|
for x in matches:
|
||||||
for x in matches:
|
x.getparent().remove(x)
|
||||||
x.getparent().remove(x)
|
return
|
||||||
return
|
|
||||||
if not matches:
|
if not matches:
|
||||||
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'}
|
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'}
|
||||||
matches = [self.create_metadata_element('identifier',
|
matches = [self.create_metadata_element('identifier',
|
||||||
@ -987,11 +989,14 @@ class OPF(object): # {{{
|
|||||||
def smart_update(self, mi, replace_metadata=False):
|
def smart_update(self, mi, replace_metadata=False):
|
||||||
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
||||||
'publisher', 'series', 'series_index', 'rating',
|
'publisher', 'series', 'series_index', 'rating',
|
||||||
'isbn', 'language', 'tags', 'category', 'comments',
|
'isbn', 'tags', 'category', 'comments',
|
||||||
'pubdate'):
|
'pubdate'):
|
||||||
val = getattr(mi, attr, None)
|
val = getattr(mi, attr, None)
|
||||||
if val is not None and val != [] and val != (None, None):
|
if val is not None and val != [] and val != (None, None):
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
lang = getattr(mi, 'language', None)
|
||||||
|
if lang and lang != 'und':
|
||||||
|
self.language = lang
|
||||||
temp = self.to_book_metadata()
|
temp = self.to_book_metadata()
|
||||||
temp.smart_update(mi, replace_metadata=replace_metadata)
|
temp.smart_update(mi, replace_metadata=replace_metadata)
|
||||||
self._user_metadata_ = temp.get_all_user_metadata(True)
|
self._user_metadata_ = temp.get_all_user_metadata(True)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
@ -42,11 +42,10 @@ class MOBIOutput(OutputFormatPlugin):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def check_for_periodical(self):
|
def check_for_periodical(self):
|
||||||
if self.oeb.metadata.publication_type and \
|
if self.is_periodical:
|
||||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:'):
|
self.periodicalize_toc()
|
||||||
self.periodicalize_toc()
|
self.check_for_masthead()
|
||||||
self.check_for_masthead()
|
self.opts.mobi_periodical = True
|
||||||
self.opts.mobi_periodical = True
|
|
||||||
else:
|
else:
|
||||||
self.opts.mobi_periodical = False
|
self.opts.mobi_periodical = False
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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,
|
||||||
@ -515,7 +515,7 @@ class FileDialog(QObject):
|
|||||||
if f and os.path.exists(f):
|
if f and os.path.exists(f):
|
||||||
self.selected_files.append(f)
|
self.selected_files.append(f)
|
||||||
else:
|
else:
|
||||||
opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.DirectoryOnly else QFileDialog.Option()
|
opts = QFileDialog.ShowDirsOnly if mode == QFileDialog.Directory else QFileDialog.Option()
|
||||||
f = unicode(QFileDialog.getExistingDirectory(parent, title, initial_dir, opts))
|
f = unicode(QFileDialog.getExistingDirectory(parent, title, initial_dir, opts))
|
||||||
if os.path.exists(f):
|
if os.path.exists(f):
|
||||||
self.selected_files.append(f)
|
self.selected_files.append(f)
|
||||||
@ -535,7 +535,7 @@ class FileDialog(QObject):
|
|||||||
|
|
||||||
def choose_dir(window, name, title, default_dir='~'):
|
def choose_dir(window, name, title, default_dir='~'):
|
||||||
fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
|
fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
|
||||||
parent=window, name=name, mode=QFileDialog.DirectoryOnly,
|
parent=window, name=name, mode=QFileDialog.Directory,
|
||||||
default_dir=default_dir)
|
default_dir=default_dir)
|
||||||
dir = fd.get_files()
|
dir = fd.get_files()
|
||||||
if dir:
|
if dir:
|
||||||
|
@ -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()
|
||||||
|
@ -429,7 +429,38 @@ class BulkBase(Base):
|
|||||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||||
|
|
||||||
class BulkBool(BulkBase, Bool):
|
class BulkBool(BulkBase, Bool):
|
||||||
pass
|
|
||||||
|
def get_initial_value(self, book_ids):
|
||||||
|
value = None
|
||||||
|
for book_id in book_ids:
|
||||||
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
|
val = False
|
||||||
|
if value is not None and value != val:
|
||||||
|
return None
|
||||||
|
value = val
|
||||||
|
return value
|
||||||
|
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||||
|
QComboBox(parent)]
|
||||||
|
w = self.widgets[1]
|
||||||
|
items = [_('Yes'), _('No'), _('Undefined')]
|
||||||
|
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||||
|
for icon, text in zip(icons, items):
|
||||||
|
w.addItem(QIcon(icon), text)
|
||||||
|
|
||||||
|
def setter(self, val):
|
||||||
|
val = {None: 2, False: 1, True: 0}[val]
|
||||||
|
self.widgets[1].setCurrentIndex(val)
|
||||||
|
|
||||||
|
def commit(self, book_ids, notify=False):
|
||||||
|
val = self.gui_val
|
||||||
|
val = self.normalize_ui_val(val)
|
||||||
|
if val != self.initial_val:
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
|
val = False
|
||||||
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||||
|
|
||||||
class BulkInt(BulkBase, Int):
|
class BulkInt(BulkBase, Int):
|
||||||
pass
|
pass
|
||||||
|
@ -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'), '')
|
||||||
|
@ -3,11 +3,16 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
||||||
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
||||||
QLineEdit
|
QLineEdit, Qt
|
||||||
|
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.library.check_library import CheckLibrary, CHECKS
|
from calibre.library.check_library import CheckLibrary, CHECKS
|
||||||
|
from calibre.library.database2 import delete_file, delete_tree
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
class Item(QTreeWidgetItem):
|
class Item(QTreeWidgetItem):
|
||||||
pass
|
pass
|
||||||
@ -24,24 +29,25 @@ class CheckLibraryDialog(QDialog):
|
|||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
|
|
||||||
self.log = QTreeWidget(self)
|
self.log = QTreeWidget(self)
|
||||||
|
self.log.itemChanged.connect(self.item_changed)
|
||||||
self._layout.addWidget(self.log)
|
self._layout.addWidget(self.log)
|
||||||
|
|
||||||
self.check = QPushButton(_('Run the check'))
|
self.check = QPushButton(_('&Run the check'))
|
||||||
self.check.setDefault(False)
|
self.check.setDefault(False)
|
||||||
self.check.clicked.connect(self.run_the_check)
|
self.check.clicked.connect(self.run_the_check)
|
||||||
self.copy = QPushButton(_('Copy to clipboard'))
|
self.copy = QPushButton(_('Copy &to clipboard'))
|
||||||
self.copy.setDefault(False)
|
self.copy.setDefault(False)
|
||||||
self.copy.clicked.connect(self.copy_to_clipboard)
|
self.copy.clicked.connect(self.copy_to_clipboard)
|
||||||
self.ok = QPushButton('&Done')
|
self.ok = QPushButton('&Done')
|
||||||
self.ok.setDefault(True)
|
self.ok.setDefault(True)
|
||||||
self.ok.clicked.connect(self.accept)
|
self.ok.clicked.connect(self.accept)
|
||||||
self.cancel = QPushButton('&Cancel')
|
self.delete = QPushButton('Delete &marked')
|
||||||
self.cancel.setDefault(False)
|
self.delete.setDefault(False)
|
||||||
self.cancel.clicked.connect(self.reject)
|
self.delete.clicked.connect(self.delete_marked)
|
||||||
self.bbox = QDialogButtonBox(self)
|
self.bbox = QDialogButtonBox(self)
|
||||||
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
|
||||||
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
|
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
|
||||||
self.bbox.addButton(self.cancel, QDialogButtonBox.RejectRole)
|
self.bbox.addButton(self.delete, QDialogButtonBox.ActionRole)
|
||||||
|
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
|
||||||
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
|
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
|
||||||
|
|
||||||
h = QHBoxLayout()
|
h = QHBoxLayout()
|
||||||
@ -83,35 +89,70 @@ class CheckLibraryDialog(QDialog):
|
|||||||
plaintext = []
|
plaintext = []
|
||||||
|
|
||||||
def builder(tree, checker, check):
|
def builder(tree, checker, check):
|
||||||
attr = check[0]
|
attr, h, checkable = check
|
||||||
list = getattr(checker, attr, None)
|
list = getattr(checker, attr, None)
|
||||||
if list is None:
|
if list is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
h = check[1]
|
|
||||||
tl = Item([h])
|
tl = Item([h])
|
||||||
for problem in list:
|
for problem in list:
|
||||||
it = Item()
|
it = Item()
|
||||||
|
if checkable:
|
||||||
|
it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
|
||||||
|
it.setCheckState(1, False)
|
||||||
|
else:
|
||||||
|
it.setFlags(Qt.ItemIsEnabled)
|
||||||
it.setText(0, problem[0])
|
it.setText(0, problem[0])
|
||||||
it.setText(1, problem[1])
|
it.setText(1, problem[1])
|
||||||
p = ', '.join(problem[2])
|
|
||||||
it.setText(2, p)
|
|
||||||
tl.addChild(it)
|
tl.addChild(it)
|
||||||
plaintext.append(','.join([h, problem[0], problem[1], p]))
|
self.all_items.append(it)
|
||||||
|
plaintext.append(','.join([h, problem[0], problem[1]]))
|
||||||
tree.addTopLevelItem(tl)
|
tree.addTopLevelItem(tl)
|
||||||
|
|
||||||
t = self.log
|
t = self.log
|
||||||
t.clear()
|
t.clear()
|
||||||
t.setColumnCount(3);
|
t.setColumnCount(2);
|
||||||
t.setHeaderLabels([_('Name'), _('Path from library'), _('Additional Information')])
|
t.setHeaderLabels([_('Name'), _('Path from library')])
|
||||||
|
self.all_items = []
|
||||||
for check in CHECKS:
|
for check in CHECKS:
|
||||||
builder(t, checker, check)
|
builder(t, checker, check)
|
||||||
|
|
||||||
t.setColumnWidth(0, 200)
|
t.setColumnWidth(0, 200)
|
||||||
t.setColumnWidth(1, 400)
|
t.setColumnWidth(1, 400)
|
||||||
|
self.delete.setEnabled(False)
|
||||||
self.text_results = '\n'.join(plaintext)
|
self.text_results = '\n'.join(plaintext)
|
||||||
|
|
||||||
|
def item_changed(self, item, column):
|
||||||
|
for it in self.all_items:
|
||||||
|
if it.checkState(1):
|
||||||
|
self.delete.setEnabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def delete_marked(self):
|
||||||
|
if not confirm('<p>'+_('The marked files and folders will be '
|
||||||
|
'<b>permanently deleted</b>. Are you sure?')
|
||||||
|
+'</p>', 'check_library_editor_delete', self):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort the paths in reverse length order so that we can be sure that
|
||||||
|
# if an item is in another item, the sub-item will be deleted first.
|
||||||
|
items = sorted(self.all_items,
|
||||||
|
key=lambda x: len(x.text(1)),
|
||||||
|
reverse=True)
|
||||||
|
for it in items:
|
||||||
|
if it.checkState(1):
|
||||||
|
try:
|
||||||
|
p = os.path.join(self.db.library_path ,unicode(it.text(1)))
|
||||||
|
if os.path.isdir(p):
|
||||||
|
delete_tree(p)
|
||||||
|
else:
|
||||||
|
delete_file(p)
|
||||||
|
except:
|
||||||
|
prints('failed to delete',
|
||||||
|
os.path.join(self.db.library_path,
|
||||||
|
unicode(it.text(1))))
|
||||||
|
self.run_the_check()
|
||||||
|
|
||||||
def copy_to_clipboard(self):
|
def copy_to_clipboard(self):
|
||||||
QApplication.clipboard().setText(self.text_results)
|
QApplication.clipboard().setText(self.text_results)
|
||||||
|
|
||||||
|
@ -190,7 +190,8 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
if self.model.rowCount() < 1:
|
if self.model.rowCount() < 1:
|
||||||
info_dialog(self, _('No metadata found'),
|
info_dialog(self, _('No metadata found'),
|
||||||
_('No metadata found, try adjusting the title and author '
|
_('No metadata found, try adjusting the title and author '
|
||||||
'or the ISBN key.')).exec_()
|
'and/or removing the ISBN.')).exec_()
|
||||||
|
self.reject()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.matches.setModel(self.model)
|
self.matches.setModel(self.model)
|
||||||
|
@ -16,6 +16,7 @@ from calibre.gui2.custom_column_widgets import populate_metadata_page
|
|||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.utils.config import dynamic
|
from calibre.utils.config import dynamic
|
||||||
|
from calibre.utils.titlecase import titlecase
|
||||||
|
|
||||||
class MyBlockingBusy(QDialog):
|
class MyBlockingBusy(QDialog):
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ class MyBlockingBusy(QDialog):
|
|||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
self.args = args
|
self.args = args
|
||||||
|
self.series_start_value = None
|
||||||
self.db = db
|
self.db = db
|
||||||
self.ids = ids
|
self.ids = ids
|
||||||
self.error = None
|
self.error = None
|
||||||
@ -115,7 +117,7 @@ class MyBlockingBusy(QDialog):
|
|||||||
aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
||||||
new_title = authors_to_string(aum)
|
new_title = authors_to_string(aum)
|
||||||
if do_title_case:
|
if do_title_case:
|
||||||
new_title = new_title.title()
|
new_title = titlecase(new_title)
|
||||||
self.db.set_title(id, new_title, notify=False)
|
self.db.set_title(id, new_title, notify=False)
|
||||||
title_set = True
|
title_set = True
|
||||||
if title:
|
if title:
|
||||||
@ -123,7 +125,7 @@ class MyBlockingBusy(QDialog):
|
|||||||
self.db.set_authors(id, new_authors, notify=False)
|
self.db.set_authors(id, new_authors, notify=False)
|
||||||
if do_title_case and not title_set:
|
if do_title_case and not title_set:
|
||||||
title = self.db.title(id, index_is_id=True)
|
title = self.db.title(id, index_is_id=True)
|
||||||
self.db.set_title(id, title.title(), notify=False)
|
self.db.set_title(id, titlecase(title), notify=False)
|
||||||
if au:
|
if au:
|
||||||
self.db.set_authors(id, string_to_authors(au), notify=False)
|
self.db.set_authors(id, string_to_authors(au), notify=False)
|
||||||
elif self.current_phase == 2:
|
elif self.current_phase == 2:
|
||||||
@ -147,8 +149,10 @@ class MyBlockingBusy(QDialog):
|
|||||||
|
|
||||||
if do_series:
|
if do_series:
|
||||||
if do_series_restart:
|
if do_series_restart:
|
||||||
next = series_start_value
|
if self.series_start_value is None:
|
||||||
series_start_value += 1
|
self.series_start_value = series_start_value
|
||||||
|
next = self.series_start_value
|
||||||
|
self.series_start_value += 1
|
||||||
else:
|
else:
|
||||||
next = self.db.get_next_series_num_for(series)
|
next = self.db.get_next_series_num_for(series)
|
||||||
self.db.set_series(id, series, notify=False, commit=False)
|
self.db.set_series(id, series, notify=False, commit=False)
|
||||||
@ -179,7 +183,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
s_r_functions = { '' : lambda x: x,
|
s_r_functions = { '' : lambda x: x,
|
||||||
_('Lower Case') : lambda x: x.lower(),
|
_('Lower Case') : lambda x: x.lower(),
|
||||||
_('Upper Case') : lambda x: x.upper(),
|
_('Upper Case') : lambda x: x.upper(),
|
||||||
_('Title Case') : lambda x: x.title(),
|
_('Title Case') : lambda x: titlecase(x),
|
||||||
}
|
}
|
||||||
|
|
||||||
s_r_match_modes = [ _('Character match'),
|
s_r_match_modes = [ _('Character match'),
|
||||||
|
@ -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)
|
||||||
@ -783,18 +785,22 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.db.set_rating(id, val)
|
self.db.set_rating(id, val)
|
||||||
elif column == 'series':
|
elif column == 'series':
|
||||||
val = val.strip()
|
val = val.strip()
|
||||||
pat = re.compile(r'\[([.0-9]+)\]')
|
if not val:
|
||||||
match = pat.search(val)
|
|
||||||
if match is not None:
|
|
||||||
self.db.set_series_index(id, float(match.group(1)))
|
|
||||||
val = pat.sub('', val).strip()
|
|
||||||
elif val:
|
|
||||||
if tweaks['series_index_auto_increment'] == 'next':
|
|
||||||
ni = self.db.get_next_series_num_for(val)
|
|
||||||
if ni != 1:
|
|
||||||
self.db.set_series_index(id, ni)
|
|
||||||
if val:
|
|
||||||
self.db.set_series(id, val)
|
self.db.set_series(id, val)
|
||||||
|
self.db.set_series_index(id, 1.0)
|
||||||
|
else:
|
||||||
|
pat = re.compile(r'\[([.0-9]+)\]')
|
||||||
|
match = pat.search(val)
|
||||||
|
if match is not None:
|
||||||
|
self.db.set_series_index(id, float(match.group(1)))
|
||||||
|
val = pat.sub('', val).strip()
|
||||||
|
elif val:
|
||||||
|
if tweaks['series_index_auto_increment'] == 'next':
|
||||||
|
ni = self.db.get_next_series_num_for(val)
|
||||||
|
if ni != 1:
|
||||||
|
self.db.set_series_index(id, ni)
|
||||||
|
if val:
|
||||||
|
self.db.set_series(id, val)
|
||||||
elif column == 'timestamp':
|
elif column == 'timestamp':
|
||||||
if val.isNull() or not val.isValid():
|
if val.isNull() or not val.isValid():
|
||||||
return False
|
return False
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<ui version="4.0" >
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
<class>MainWindow</class>
|
<class>MainWindow</class>
|
||||||
<widget class="QMainWindow" name="MainWindow" >
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
<property name="geometry" >
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
@ -9,75 +10,51 @@
|
|||||||
<height>701</height>
|
<height>701</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy" >
|
<property name="sizePolicy">
|
||||||
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
|
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle" >
|
<property name="windowTitle">
|
||||||
<string>LRF Viewer</string>
|
<string>LRF Viewer</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon" >
|
<property name="windowIcon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/viewer.png</normaloff>:/images/viewer.png</iconset>
|
<normaloff>:/images/viewer.png</normaloff>:/images/viewer.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="central_widget" >
|
<widget class="QWidget" name="central_widget">
|
||||||
<property name="geometry" >
|
<layout class="QVBoxLayout">
|
||||||
<rect>
|
<property name="margin">
|
||||||
<x>0</x>
|
|
||||||
<y>39</y>
|
|
||||||
<width>601</width>
|
|
||||||
<height>662</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" >
|
|
||||||
<property name="margin" >
|
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QStackedWidget" name="stack" >
|
<widget class="QStackedWidget" name="stack">
|
||||||
<property name="currentIndex" >
|
<property name="currentIndex">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="viewer_page" >
|
<widget class="QWidget" name="viewer_page">
|
||||||
<property name="geometry" >
|
<layout class="QGridLayout">
|
||||||
<rect>
|
<property name="margin">
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>601</width>
|
|
||||||
<height>662</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" >
|
|
||||||
<property name="margin" >
|
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="0" column="0" >
|
<item row="0" column="0">
|
||||||
<widget class="BookView" name="graphics_view" >
|
<widget class="BookView" name="graphics_view">
|
||||||
<property name="mouseTracking" >
|
<property name="mouseTracking">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="bar_page" >
|
<widget class="QWidget" name="bar_page">
|
||||||
<property name="geometry" >
|
<layout class="QVBoxLayout">
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>601</width>
|
|
||||||
<height>701</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" >
|
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer>
|
||||||
<property name="orientation" >
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0" >
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>20</width>
|
<width>20</width>
|
||||||
<height>40</height>
|
<height>40</height>
|
||||||
@ -86,34 +63,34 @@
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QFrame" name="frame_2" >
|
<widget class="QFrame" name="frame_2">
|
||||||
<property name="frameShape" >
|
<property name="frameShape">
|
||||||
<enum>QFrame::StyledPanel</enum>
|
<enum>QFrame::StyledPanel</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="frameShadow" >
|
<property name="frameShadow">
|
||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" >
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QProgressBar" name="progress_bar" >
|
<widget class="QProgressBar" name="progress_bar">
|
||||||
<property name="maximum" >
|
<property name="maximum">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="value" >
|
<property name="value">
|
||||||
<number>-1</number>
|
<number>-1</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="progress_label" >
|
<widget class="QLabel" name="progress_label">
|
||||||
<property name="font" >
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>11</pointsize>
|
<pointsize>11</pointsize>
|
||||||
<weight>75</weight>
|
<weight>75</weight>
|
||||||
<bold>true</bold>
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Parsing LRF file</string>
|
<string>Parsing LRF file</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -123,10 +100,10 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer>
|
||||||
<property name="orientation" >
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0" >
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>20</width>
|
<width>20</width>
|
||||||
<height>40</height>
|
<height>40</height>
|
||||||
@ -140,93 +117,85 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar" >
|
<widget class="QToolBar" name="tool_bar">
|
||||||
<property name="geometry" >
|
<property name="windowTitle">
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>601</width>
|
|
||||||
<height>39</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle" >
|
|
||||||
<string>LRF Viewer toolbar</string>
|
<string>LRF Viewer toolbar</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="allowedAreas" >
|
<property name="allowedAreas">
|
||||||
<set>Qt::AllToolBarAreas</set>
|
<set>Qt::AllToolBarAreas</set>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="toolBarArea" >
|
<attribute name="toolBarArea">
|
||||||
<enum>TopToolBarArea</enum>
|
<enum>Qt::TopToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak" >
|
<attribute name="toolBarBreak">
|
||||||
<bool>true</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="action_back" />
|
<addaction name="action_back"/>
|
||||||
<addaction name="action_forward" />
|
<addaction name="action_forward"/>
|
||||||
<addaction name="separator" />
|
<addaction name="separator"/>
|
||||||
<addaction name="action_open_ebook" />
|
<addaction name="action_open_ebook"/>
|
||||||
<addaction name="action_configure" />
|
<addaction name="action_configure"/>
|
||||||
<addaction name="separator" />
|
<addaction name="separator"/>
|
||||||
<addaction name="action_previous_page" />
|
<addaction name="action_previous_page"/>
|
||||||
<addaction name="action_next_page" />
|
<addaction name="action_next_page"/>
|
||||||
<addaction name="separator" />
|
<addaction name="separator"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="action_next_page" >
|
<action name="action_next_page">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/next.png</normaloff>:/images/next.png</iconset>
|
<normaloff>:/images/next.png</normaloff>:/images/next.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Next Page</string>
|
<string>Next Page</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_previous_page" >
|
<action name="action_previous_page">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/previous.png</normaloff>:/images/previous.png</iconset>
|
<normaloff>:/images/previous.png</normaloff>:/images/previous.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Previous Page</string>
|
<string>Previous Page</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_back" >
|
<action name="action_back">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
|
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Back</string>
|
<string>Back</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_forward" >
|
<action name="action_forward">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Forward</string>
|
<string>Forward</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_next_match" >
|
<action name="action_next_match">
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Next match</string>
|
<string>Next match</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_open_ebook" >
|
<action name="action_open_ebook">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Open ebook</string>
|
<string>Open ebook</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_configure" >
|
<action name="action_configure">
|
||||||
<property name="icon" >
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc" >
|
<iconset>
|
||||||
<normaloff>:/images/config.png</normaloff>:/images/config.png</iconset>
|
<normaloff>:/images/config.png</normaloff>:/images/config.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text" >
|
<property name="text">
|
||||||
<string>Configure</string>
|
<string>Configure</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
@ -239,7 +208,7 @@
|
|||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc" />
|
<include location="../../../../resources/images.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -365,6 +365,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
|
|||||||
except:
|
except:
|
||||||
olddb = None
|
olddb = None
|
||||||
db = LibraryDatabase2(newloc)
|
db = LibraryDatabase2(newloc)
|
||||||
|
if self.content_server is not None:
|
||||||
|
self.content_server.set_database(db)
|
||||||
self.library_path = newloc
|
self.library_path = newloc
|
||||||
self.book_on_device(None, reset=True)
|
self.book_on_device(None, reset=True)
|
||||||
db.set_book_on_device_func(self.book_on_device)
|
db.set_book_on_device_func(self.book_on_device)
|
||||||
|
@ -716,6 +716,9 @@ View an ebook.
|
|||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
|
# Ensure viewer can continue to function if GUI is closed
|
||||||
|
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
|
||||||
|
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
opts, args = parser.parse_args(args)
|
opts, args = parser.parse_args(args)
|
||||||
pid = os.fork() if False and (islinux or isfreebsd) else -1
|
pid = os.fork() if False and (islinux or isfreebsd) else -1
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="toolBarArea">
|
<attribute name="toolBarArea">
|
||||||
<enum>LeftToolBarArea</enum>
|
<enum>Qt::LeftToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -136,7 +136,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar2">
|
<widget class="QToolBar" name="tool_bar2">
|
||||||
<attribute name="toolBarArea">
|
<attribute name="toolBarArea">
|
||||||
<enum>TopToolBarArea</enum>
|
<enum>Qt::TopToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -380,7 +380,7 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
field_count = 3
|
field_count = 3
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
qd = parse_date(query)
|
qd = parse_date(query, as_utc=False)
|
||||||
except:
|
except:
|
||||||
raise ParseException(query, len(query), 'Date conversion error', self)
|
raise ParseException(query, len(query), 'Date conversion error', self)
|
||||||
if '-' in query:
|
if '-' in query:
|
||||||
@ -816,6 +816,10 @@ class SortKeyGenerator(object):
|
|||||||
if val is None:
|
if val is None:
|
||||||
val = ''
|
val = ''
|
||||||
val = val.lower()
|
val = val.lower()
|
||||||
|
|
||||||
|
elif dt == 'bool':
|
||||||
|
val = {True: 1, False: 2, None: 3}.get(val, 3)
|
||||||
|
|
||||||
yield val
|
yield val
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -14,14 +14,14 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
|||||||
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
|
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
|
||||||
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
|
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
|
||||||
|
|
||||||
CHECKS = [('invalid_titles', _('Invalid titles')),
|
CHECKS = [('invalid_titles', _('Invalid titles'), True),
|
||||||
('extra_titles', _('Extra titles')),
|
('extra_titles', _('Extra titles'), True),
|
||||||
('invalid_authors', _('Invalid authors')),
|
('invalid_authors', _('Invalid authors'), True),
|
||||||
('extra_authors', _('Extra authors')),
|
('extra_authors', _('Extra authors'), True),
|
||||||
('missing_formats', _('Missing book formats')),
|
('missing_formats', _('Missing book formats'), False),
|
||||||
('extra_formats', _('Extra book formats')),
|
('extra_formats', _('Extra book formats'), True),
|
||||||
('extra_files', _('Unknown files in books')),
|
('extra_files', _('Unknown files in books'), True),
|
||||||
('failed_folders', _('Folders raising exception'))
|
('failed_folders', _('Folders raising exception'), False)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -41,7 +41,6 @@ class CheckLibrary(object):
|
|||||||
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
|
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
|
||||||
|
|
||||||
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
|
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
|
||||||
self.bad_ext_pat = re.compile(r'[^a-z0-9]+')
|
|
||||||
|
|
||||||
self.dirs = []
|
self.dirs = []
|
||||||
self.book_dirs = []
|
self.book_dirs = []
|
||||||
@ -78,7 +77,7 @@ class CheckLibrary(object):
|
|||||||
auth_path = os.path.join(lib, auth_dir)
|
auth_path = os.path.join(lib, auth_dir)
|
||||||
# First check: author must be a directory
|
# First check: author must be a directory
|
||||||
if not os.path.isdir(auth_path):
|
if not os.path.isdir(auth_path):
|
||||||
self.invalid_authors.append((auth_dir, auth_dir, []))
|
self.invalid_authors.append((auth_dir, auth_dir))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.potential_authors[auth_dir] = {}
|
self.potential_authors[auth_dir] = {}
|
||||||
@ -93,7 +92,7 @@ class CheckLibrary(object):
|
|||||||
m = self.db_id_regexp.search(title_dir)
|
m = self.db_id_regexp.search(title_dir)
|
||||||
# Second check: title must have an ID and must be a directory
|
# Second check: title must have an ID and must be a directory
|
||||||
if m is None or not os.path.isdir(title_path):
|
if m is None or not os.path.isdir(title_path):
|
||||||
self.invalid_titles.append((auth_dir, db_path, [title_dir]))
|
self.invalid_titles.append((auth_dir, db_path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
id = m.group(1)
|
id = m.group(1)
|
||||||
@ -101,12 +100,12 @@ class CheckLibrary(object):
|
|||||||
if self.is_case_sensitive:
|
if self.is_case_sensitive:
|
||||||
if int(id) not in self.all_ids or \
|
if int(id) not in self.all_ids or \
|
||||||
db_path not in self.all_dbpaths:
|
db_path not in self.all_dbpaths:
|
||||||
self.extra_titles.append((title_dir, db_path, []))
|
self.extra_titles.append((title_dir, db_path))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if int(id) not in self.all_ids or \
|
if int(id) not in self.all_ids or \
|
||||||
db_path.lower() not in self.all_lc_dbpaths:
|
db_path.lower() not in self.all_lc_dbpaths:
|
||||||
self.extra_titles.append((title_dir, db_path, []))
|
self.extra_titles.append((title_dir, db_path))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Record the book to check its formats
|
# Record the book to check its formats
|
||||||
@ -115,7 +114,7 @@ class CheckLibrary(object):
|
|||||||
|
|
||||||
# Fourth check: author directories that contain no titles
|
# Fourth check: author directories that contain no titles
|
||||||
if not found_titles:
|
if not found_titles:
|
||||||
self.extra_authors.append((auth_dir, auth_dir, []))
|
self.extra_authors.append((auth_dir, auth_dir))
|
||||||
|
|
||||||
for x in self.book_dirs:
|
for x in self.book_dirs:
|
||||||
try:
|
try:
|
||||||
@ -132,9 +131,7 @@ class CheckLibrary(object):
|
|||||||
ext = ext[1:].lower()
|
ext = ext[1:].lower()
|
||||||
if ext in EBOOK_EXTENSIONS:
|
if ext in EBOOK_EXTENSIONS:
|
||||||
return True
|
return True
|
||||||
if self.bad_ext_pat.search(ext) is not None:
|
return False
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process_book(self, lib, book_info):
|
def process_book(self, lib, book_info):
|
||||||
(db_path, title_dir, book_id) = book_info
|
(db_path, title_dir, book_id) = book_info
|
||||||
@ -148,18 +145,18 @@ class CheckLibrary(object):
|
|||||||
if self.is_case_sensitive:
|
if self.is_case_sensitive:
|
||||||
unknowns = frozenset(filenames-formats-NORMALS)
|
unknowns = frozenset(filenames-formats-NORMALS)
|
||||||
# Check: any books that aren't formats or normally there?
|
# Check: any books that aren't formats or normally there?
|
||||||
if unknowns:
|
for u in unknowns:
|
||||||
self.extra_files.append((title_dir, db_path, unknowns))
|
self.extra_files.append((title_dir, os.path.join(db_path, u)))
|
||||||
|
|
||||||
# Check: any book formats that should be there?
|
# Check: any book formats that should be there?
|
||||||
missing = book_formats - formats
|
missing = book_formats - formats
|
||||||
if missing:
|
for m in missing:
|
||||||
self.missing_formats.append((title_dir, db_path, missing))
|
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||||
|
|
||||||
# Check: any book formats that shouldn't be there?
|
# Check: any book formats that shouldn't be there?
|
||||||
extra = formats - book_formats - NORMALS
|
extra = formats - book_formats - NORMALS
|
||||||
if extra:
|
for e in extra:
|
||||||
self.extra_formats.append((title_dir, db_path, extra))
|
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||||
else:
|
else:
|
||||||
def lc_map(fnames, fset):
|
def lc_map(fnames, fset):
|
||||||
m = {}
|
m = {}
|
||||||
@ -171,19 +168,16 @@ class CheckLibrary(object):
|
|||||||
formats_lc = frozenset([f.lower() for f in formats])
|
formats_lc = frozenset([f.lower() for f in formats])
|
||||||
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
|
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
|
||||||
# Check: any books that aren't formats or normally there?
|
# Check: any books that aren't formats or normally there?
|
||||||
if unknowns:
|
for f in lc_map(filenames, unknowns):
|
||||||
self.extra_files.append((title_dir, db_path,
|
self.extra_files.append((title_dir, os.path.join(db_path, f)))
|
||||||
lc_map(filenames, unknowns)))
|
|
||||||
|
|
||||||
book_formats_lc = frozenset([f.lower() for f in book_formats])
|
book_formats_lc = frozenset([f.lower() for f in book_formats])
|
||||||
# Check: any book formats that should be there?
|
# Check: any book formats that should be there?
|
||||||
missing = book_formats_lc - formats_lc
|
missing = book_formats_lc - formats_lc
|
||||||
if missing:
|
for m in lc_map(book_formats, missing):
|
||||||
self.missing_formats.append((title_dir, db_path,
|
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
|
||||||
lc_map(book_formats, missing)))
|
|
||||||
|
|
||||||
# Check: any book formats that shouldn't be there?
|
# Check: any book formats that shouldn't be there?
|
||||||
extra = formats_lc - book_formats_lc - NORMALS
|
extra = formats_lc - book_formats_lc - NORMALS
|
||||||
if extra:
|
for e in lc_map(formats, extra):
|
||||||
self.extra_formats.append((title_dir, db_path,
|
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
|
||||||
lc_map(formats, extra)))
|
|
||||||
|
@ -943,11 +943,11 @@ def command_check_library(args, dbpath):
|
|||||||
return
|
return
|
||||||
if opts.csv:
|
if opts.csv:
|
||||||
for i in list:
|
for i in list:
|
||||||
print check[1] + ',' + i[0] + ',' + i[1] + ',' + '|'.join(i[2])
|
print check[1] + ',' + i[0] + ',' + i[1]
|
||||||
else:
|
else:
|
||||||
print check[1]
|
print check[1]
|
||||||
for i in list:
|
for i in list:
|
||||||
print ' %-30.30s - %-30.30s - %s'%(i[0], i[1], ', '.join(i[2]))
|
print ' %-40.40s - %-40.40s'%(i[0], i[1])
|
||||||
|
|
||||||
db = LibraryDatabase2(dbpath)
|
db = LibraryDatabase2(dbpath)
|
||||||
checker = CheckLibrary(dbpath, db)
|
checker = CheckLibrary(dbpath, db)
|
||||||
|
@ -653,17 +653,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.dirtied(book_ids)
|
self.dirtied(book_ids)
|
||||||
|
|
||||||
def get_metadata_for_dump(self, idx, remove_from_dirtied=True):
|
def get_metadata_for_dump(self, idx, remove_from_dirtied=True):
|
||||||
|
path, mi = (None, None)
|
||||||
try:
|
try:
|
||||||
path = os.path.join(self.abspath(idx, index_is_id=True), 'metadata.opf')
|
# While a book is being created, the path is empty. Don't bother to
|
||||||
mi = self.get_metadata(idx, index_is_id=True)
|
# try to write the opf, because it will go to the wrong folder.
|
||||||
# Always set cover to cover.jpg. Even if cover doesn't exist,
|
if self.path(idx, index_is_id=True):
|
||||||
# no harm done. This way no need to call dirtied when
|
path = os.path.join(self.abspath(idx, index_is_id=True), 'metadata.opf')
|
||||||
# cover is set/removed
|
mi = self.get_metadata(idx, index_is_id=True)
|
||||||
mi.cover = 'cover.jpg'
|
# Always set cover to cover.jpg. Even if cover doesn't exist,
|
||||||
|
# no harm done. This way no need to call dirtied when
|
||||||
|
# cover is set/removed
|
||||||
|
mi.cover = 'cover.jpg'
|
||||||
except:
|
except:
|
||||||
# This almost certainly means that the book has been deleted while
|
# This almost certainly means that the book has been deleted while
|
||||||
# the backup operation sat in the queue.
|
# the backup operation sat in the queue.
|
||||||
path, mi = (None, None)
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# clear the dirtied indicator. The user must put it back if
|
# clear the dirtied indicator. The user must put it back if
|
||||||
@ -748,10 +752,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def find_identical_books(self, mi):
|
def find_identical_books(self, mi):
|
||||||
fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
|
fuzzy_title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||||
[
|
[
|
||||||
(r'[\[\](){}<>\'";,:#]', ''),
|
(r'[\[\](){}<>\'";,:#]', ''),
|
||||||
(r'^(the|a|an) ', ''),
|
(tweaks.get('title_sort_articles', r'^(a|the|an)\s+'), ''),
|
||||||
(r'[-._]', ' '),
|
(r'[-._]', ' '),
|
||||||
(r'\s+', ' ')
|
(r'\s+', ' ')
|
||||||
]
|
]
|
||||||
|
@ -71,9 +71,17 @@ class Restore(Thread):
|
|||||||
|
|
||||||
if self.conflicting_custom_cols:
|
if self.conflicting_custom_cols:
|
||||||
ans += '\n\n'
|
ans += '\n\n'
|
||||||
ans += 'The following custom columns were not fully restored:\n'
|
ans += 'The following custom columns have conflicting definitions ' \
|
||||||
|
'and were not fully restored:\n'
|
||||||
for x in self.conflicting_custom_cols:
|
for x in self.conflicting_custom_cols:
|
||||||
ans += '\t#'+x+'\n'
|
ans += '\t#'+x+'\n'
|
||||||
|
ans += '\tused:\t%s, %s, %s, %s\n'%(self.custom_columns[x][1],
|
||||||
|
self.custom_columns[x][2],
|
||||||
|
self.custom_columns[x][3],
|
||||||
|
self.custom_columns[x][5])
|
||||||
|
for coldef in self.conflicting_custom_cols[x]:
|
||||||
|
ans += '\tother:\t%s, %s, %s, %s\n'%(coldef[1], coldef[2],
|
||||||
|
coldef[3], coldef[5])
|
||||||
|
|
||||||
if self.mismatched_dirs:
|
if self.mismatched_dirs:
|
||||||
ans += '\n\n'
|
ans += '\n\n'
|
||||||
@ -152,7 +160,7 @@ class Restore(Thread):
|
|||||||
|
|
||||||
def create_cc_metadata(self):
|
def create_cc_metadata(self):
|
||||||
self.books.sort(key=itemgetter('timestamp'))
|
self.books.sort(key=itemgetter('timestamp'))
|
||||||
m = {}
|
self.custom_columns = {}
|
||||||
fields = ('label', 'name', 'datatype', 'is_multiple', 'is_editable',
|
fields = ('label', 'name', 'datatype', 'is_multiple', 'is_editable',
|
||||||
'display')
|
'display')
|
||||||
for b in self.books:
|
for b in self.books:
|
||||||
@ -168,16 +176,17 @@ class Restore(Thread):
|
|||||||
if len(args) == len(fields):
|
if len(args) == len(fields):
|
||||||
# TODO: Do series type columns need special handling?
|
# TODO: Do series type columns need special handling?
|
||||||
label = cfm['label']
|
label = cfm['label']
|
||||||
if label in m and args != m[label]:
|
if label in self.custom_columns and args != self.custom_columns[label]:
|
||||||
if label not in self.conflicting_custom_cols:
|
if label not in self.conflicting_custom_cols:
|
||||||
self.conflicting_custom_cols[label] = set([m[label]])
|
self.conflicting_custom_cols[label] = []
|
||||||
self.conflicting_custom_cols[label].add(args)
|
if self.custom_columns[label] not in self.conflicting_custom_cols[label]:
|
||||||
m[cfm['label']] = args
|
self.conflicting_custom_cols[label].append(self.custom_columns[label])
|
||||||
|
self.custom_columns[label] = args
|
||||||
|
|
||||||
db = RestoreDatabase(self.library_path)
|
db = RestoreDatabase(self.library_path)
|
||||||
self.progress_callback(None, len(m))
|
self.progress_callback(None, len(self.custom_columns))
|
||||||
if len(m):
|
if len(self.custom_columns):
|
||||||
for i,args in enumerate(m.values()):
|
for i,args in enumerate(self.custom_columns.values()):
|
||||||
db.create_custom_column(*args)
|
db.create_custom_column(*args)
|
||||||
self.progress_callback(_('creating custom column ')+args[0], i+1)
|
self.progress_callback(_('creating custom column ')+args[0], i+1)
|
||||||
db.conn.close()
|
db.conn.close()
|
||||||
|
@ -131,15 +131,14 @@ class SafeFormat(TemplateFormatter):
|
|||||||
self.vformat(b['display']['composite_template'], [], kwargs)
|
self.vformat(b['display']['composite_template'], [], kwargs)
|
||||||
return self.composite_values[key]
|
return self.composite_values[key]
|
||||||
if key in kwargs:
|
if key in kwargs:
|
||||||
return kwargs[key].replace('/', '_').replace('\\', '_')
|
val = kwargs[key]
|
||||||
|
return val.replace('/', '_').replace('\\', '_')
|
||||||
return ''
|
return ''
|
||||||
except:
|
except:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
safe_formatter = SafeFormat()
|
|
||||||
|
|
||||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||||
to_lowercase=False):
|
to_lowercase=False):
|
||||||
@ -173,17 +172,22 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
|||||||
custom_metadata = mi.get_all_user_metadata(make_copy=False)
|
custom_metadata = mi.get_all_user_metadata(make_copy=False)
|
||||||
for key in custom_metadata:
|
for key in custom_metadata:
|
||||||
if key in format_args:
|
if key in format_args:
|
||||||
|
cm = custom_metadata[key]
|
||||||
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
|
## TODO: NEWMETA: should ratings be divided by 2? The standard rating isn't...
|
||||||
if custom_metadata[key]['datatype'] == 'series':
|
if cm['datatype'] == 'series':
|
||||||
format_args[key] = tsfmt(format_args[key])
|
format_args[key] = tsfmt(format_args[key])
|
||||||
if key+'_index' in format_args:
|
if key+'_index' in format_args:
|
||||||
format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
|
format_args[key+'_index'] = fmt_sidx(format_args[key+'_index'])
|
||||||
elif custom_metadata[key]['datatype'] == 'datetime':
|
elif cm['datatype'] == 'datetime':
|
||||||
format_args[key] = strftime(timefmt, format_args[key].timetuple())
|
format_args[key] = strftime(timefmt, format_args[key].timetuple())
|
||||||
elif custom_metadata[key]['datatype'] == 'bool':
|
elif cm['datatype'] == 'bool':
|
||||||
format_args[key] = _('yes') if format_args[key] else _('no')
|
format_args[key] = _('yes') if format_args[key] else _('no')
|
||||||
|
elif cm['datatype'] in ['int', 'float']:
|
||||||
components = safe_formatter.safe_format(template, format_args,
|
if format_args[key] != 0:
|
||||||
|
format_args[key] = unicode(format_args[key])
|
||||||
|
else:
|
||||||
|
format_args[key] = ''
|
||||||
|
components = SafeFormat().safe_format(template, format_args,
|
||||||
'G_C-EXCEPTION!', mi)
|
'G_C-EXCEPTION!', mi)
|
||||||
components = [x.strip() for x in components.split('/') if x.strip()]
|
components = [x.strip() for x in components.split('/') if x.strip()]
|
||||||
components = [sanitize_func(x) for x in components if x]
|
components = [sanitize_func(x) for x in components if x]
|
||||||
|
@ -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):
|
||||||
|
@ -10,6 +10,7 @@ import logging
|
|||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
from cherrypy.process.plugins import SimplePlugin
|
||||||
|
|
||||||
from calibre.constants import __appname__, __version__
|
from calibre.constants import __appname__, __version__
|
||||||
from calibre.utils.date import fromtimestamp
|
from calibre.utils.date import fromtimestamp
|
||||||
@ -27,16 +28,19 @@ from calibre.library.server.browse import BrowseServer
|
|||||||
|
|
||||||
class DispatchController(object): # {{{
|
class DispatchController(object): # {{{
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, prefix):
|
||||||
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 ''
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
@ -54,16 +58,45 @@ class DispatchController(object): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
class BonJour(SimplePlugin): # {{{
|
||||||
|
|
||||||
|
def __init__(self, engine, port=8080, prefix=''):
|
||||||
|
SimplePlugin.__init__(self, engine)
|
||||||
|
self.port = port
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
try:
|
||||||
|
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
||||||
|
self.port, {'path':self.prefix+'/stanza'})
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
cherrypy.log.error('Failed to start BonJour:')
|
||||||
|
cherrypy.log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
start.priority = 90
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
try:
|
||||||
|
stop_zeroconf()
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
cherrypy.log.error('Failed to stop BonJour:')
|
||||||
|
cherrypy.log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
stop.priority = 10
|
||||||
|
|
||||||
|
cherrypy.engine.bonjour = BonJour(cherrypy.engine)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
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):
|
||||||
self.db = db
|
|
||||||
for item in self.db:
|
|
||||||
item
|
|
||||||
break
|
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.embedded = embedded
|
self.embedded = embedded
|
||||||
self.state_callback = None
|
self.state_callback = None
|
||||||
@ -71,7 +104,15 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
map(int, self.opts.max_cover.split('x'))
|
map(int, self.opts.max_cover.split('x'))
|
||||||
path = P('content_server')
|
path = P('content_server')
|
||||||
self.build_time = fromtimestamp(os.stat(path).st_mtime)
|
self.build_time = fromtimestamp(os.stat(path).st_mtime)
|
||||||
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
||||||
|
|
||||||
|
cherrypy.engine.bonjour.port = opts.port
|
||||||
|
cherrypy.engine.bonjour.prefix = opts.url_prefix
|
||||||
|
|
||||||
|
Cache.__init__(self)
|
||||||
|
|
||||||
|
self.set_database(db)
|
||||||
|
|
||||||
cherrypy.config.update({
|
cherrypy.config.update({
|
||||||
'log.screen' : opts.develop,
|
'log.screen' : opts.develop,
|
||||||
'engine.autoreload_on' : opts.develop,
|
'engine.autoreload_on' : opts.develop,
|
||||||
@ -97,18 +138,28 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
|
'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
|
||||||
}
|
}
|
||||||
|
|
||||||
sr = getattr(opts, 'restriction', None)
|
|
||||||
sr = db.prefs.get('cs_restriction', '') if sr is None else sr
|
|
||||||
self.set_search_restriction(sr)
|
|
||||||
|
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.exception = None
|
self.exception = None
|
||||||
|
self.setup_loggers()
|
||||||
|
cherrypy.engine.bonjour.subscribe()
|
||||||
|
|
||||||
|
def set_database(self, db):
|
||||||
|
self.db = db
|
||||||
|
sr = getattr(self.opts, 'restriction', None)
|
||||||
|
sr = db.prefs.get('cs_restriction', '') if sr is None else sr
|
||||||
|
self.set_search_restriction(sr)
|
||||||
|
|
||||||
|
def graceful(self):
|
||||||
|
cherrypy.engine.graceful()
|
||||||
|
|
||||||
def set_search_restriction(self, restriction):
|
def set_search_restriction(self, restriction):
|
||||||
|
self.search_restriction_name = restriction
|
||||||
if restriction:
|
if restriction:
|
||||||
self.search_restriction = 'search:"%s"'%restriction
|
self.search_restriction = 'search:"%s"'%restriction
|
||||||
else:
|
else:
|
||||||
self.search_restriction = ''
|
self.search_restriction = ''
|
||||||
|
self.reset_caches()
|
||||||
|
|
||||||
def setup_loggers(self):
|
def setup_loggers(self):
|
||||||
access_file = log_access_file
|
access_file = log_access_file
|
||||||
@ -132,7 +183,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
d = DispatchController()
|
d = DispatchController(self.opts.url_prefix)
|
||||||
for x in self.__class__.__bases__:
|
for x in self.__class__.__bases__:
|
||||||
if hasattr(x, 'add_routes'):
|
if hasattr(x, 'add_routes'):
|
||||||
x.add_routes(self, d)
|
x.add_routes(self, d)
|
||||||
@ -140,7 +191,6 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
root_conf['request.dispatch'] = d.dispatcher
|
root_conf['request.dispatch'] = d.dispatcher
|
||||||
self.config['/'] = root_conf
|
self.config['/'] = root_conf
|
||||||
|
|
||||||
self.setup_loggers()
|
|
||||||
cherrypy.tree.mount(root=None, config=self.config)
|
cherrypy.tree.mount(root=None, config=self.config)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
@ -154,24 +204,14 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
|
|||||||
cherrypy.engine.start()
|
cherrypy.engine.start()
|
||||||
|
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
try:
|
#if hasattr(cherrypy.engine, 'signal_handler'):
|
||||||
publish_zeroconf('Books in calibre', '_stanza._tcp',
|
# cherrypy.engine.signal_handler.subscribe()
|
||||||
self.opts.port, {'path':'/stanza'})
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
cherrypy.log.error('Failed to start BonJour:')
|
|
||||||
cherrypy.log.error(traceback.format_exc())
|
|
||||||
cherrypy.engine.block()
|
cherrypy.engine.block()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.exception = e
|
self.exception = e
|
||||||
finally:
|
finally:
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
try:
|
|
||||||
stop_zeroconf()
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
cherrypy.log.error('Failed to stop BonJour:')
|
|
||||||
cherrypy.log.error(traceback.format_exc())
|
|
||||||
try:
|
try:
|
||||||
if callable(self.state_callback):
|
if callable(self.state_callback):
|
||||||
self.state_callback(self.is_running)
|
self.state_callback(self.is_running)
|
||||||
|
@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import operator, os, json
|
import operator, os, json
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
from urllib import quote, unquote
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ from calibre.library.comments import comments_to_html
|
|||||||
from calibre.library.server import custom_fields_to_display
|
from calibre.library.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
|
||||||
@ -34,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>
|
||||||
'''
|
'''
|
||||||
@ -48,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'''\
|
||||||
@ -90,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
|
||||||
@ -107,22 +108,25 @@ 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, db, datatype): # {{{
|
def get_category_items(category, items, restriction, datatype, prefix): # {{{
|
||||||
|
|
||||||
|
if category == 'search':
|
||||||
|
items = [x for x in items if x.name != restriction]
|
||||||
|
|
||||||
def item(i):
|
def item(i):
|
||||||
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))
|
||||||
@ -136,9 +140,9 @@ def get_category_items(category, items, db, datatype): # {{{
|
|||||||
q = i.category
|
q = i.category
|
||||||
if not q:
|
if not q:
|
||||||
q = category
|
q = category
|
||||||
href = '/browse/matches/%s/%s'%(q, 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>'])
|
||||||
@ -164,6 +168,9 @@ class Endpoint(object): # {{{
|
|||||||
sort_val = cookie[eself.sort_cookie_name].value
|
sort_val = cookie[eself.sort_cookie_name].value
|
||||||
kwargs[eself.sort_kwarg] = sort_val
|
kwargs[eself.sort_kwarg] = sort_val
|
||||||
|
|
||||||
|
# Remove AJAX caching disabling jquery workaround arg
|
||||||
|
kwargs.pop('_', None)
|
||||||
|
|
||||||
ans = func(self, *args, **kwargs)
|
ans = func(self, *args, **kwargs)
|
||||||
cherrypy.response.headers['Content-Type'] = eself.mimetype
|
cherrypy.response.headers['Content-Type'] = eself.mimetype
|
||||||
updated = self.db.last_modified()
|
updated = self.db.last_modified()
|
||||||
@ -236,6 +243,7 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
|
ans = ans.replace('{sort_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
|
||||||
@ -251,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):
|
||||||
@ -267,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)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -298,6 +306,7 @@ class BrowseServer(object):
|
|||||||
category_meta = self.db.field_metadata
|
category_meta = self.db.field_metadata
|
||||||
cats = [
|
cats = [
|
||||||
(_('Newest'), 'newest', 'forward.png'),
|
(_('Newest'), 'newest', 'forward.png'),
|
||||||
|
(_('All books'), 'allbooks', 'book.png'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def getter(x):
|
def getter(x):
|
||||||
@ -326,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(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>'\
|
||||||
@ -369,7 +378,9 @@ 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, self.db, datatype)
|
items = get_category_items(category, items,
|
||||||
|
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([])
|
||||||
@ -384,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)
|
||||||
@ -401,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)
|
||||||
@ -439,7 +451,9 @@ class BrowseServer(object):
|
|||||||
entries.append(x)
|
entries.append(x)
|
||||||
|
|
||||||
sort = self.browse_sort_categories(entries, sort)
|
sort = self.browse_sort_categories(entries, sort)
|
||||||
entries = get_category_items(category, entries, self.db, datatype)
|
entries = get_category_items(category, entries,
|
||||||
|
self.search_restriction_name, datatype,
|
||||||
|
self.opts.url_prefix)
|
||||||
return json.dumps(entries, ensure_ascii=False)
|
return json.dumps(entries, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
@ -449,7 +463,11 @@ class BrowseServer(object):
|
|||||||
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(self.opts.url_prefix +
|
||||||
|
'/browse/matches/newest/dummy')
|
||||||
|
elif category == 'allbooks':
|
||||||
|
raise cherrypy.InternalRedirect(self.opts.url_prefix +
|
||||||
|
'/browse/matches/allbooks/dummy')
|
||||||
else:
|
else:
|
||||||
ans = self.browse_category(category, category_sort)
|
ans = self.browse_category(category, category_sort)
|
||||||
|
|
||||||
@ -473,32 +491,40 @@ class BrowseServer(object):
|
|||||||
|
|
||||||
@Endpoint(sort_type='list')
|
@Endpoint(sort_type='list')
|
||||||
def browse_matches(self, category=None, cid=None, list_sort=None):
|
def browse_matches(self, category=None, cid=None, list_sort=None):
|
||||||
|
if list_sort:
|
||||||
|
list_sort = unquote(list_sort)
|
||||||
if not cid:
|
if not cid:
|
||||||
raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid)
|
raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid)
|
||||||
categories = self.categories_cache()
|
categories = self.categories_cache()
|
||||||
|
|
||||||
if category not in categories and category != 'newest':
|
if category not in categories and \
|
||||||
|
category not in ('newest', 'allbooks'):
|
||||||
raise cherrypy.HTTPError(404, 'category not found')
|
raise cherrypy.HTTPError(404, 'category not found')
|
||||||
fm = self.db.field_metadata
|
fm = self.db.field_metadata
|
||||||
try:
|
try:
|
||||||
category_name = fm[category]['name']
|
category_name = fm[category]['name']
|
||||||
dt = fm[category]['datatype']
|
dt = fm[category]['datatype']
|
||||||
except:
|
except:
|
||||||
if category != 'newest':
|
if category not in ('newest', 'allbooks'):
|
||||||
raise
|
raise
|
||||||
category_name = _('Newest')
|
category_name = {
|
||||||
|
'newest' : _('Newest'),
|
||||||
|
'allbooks' : _('All books'),
|
||||||
|
}[category]
|
||||||
dt = None
|
dt = None
|
||||||
|
|
||||||
hide_sort = 'true' if dt == 'series' else 'false'
|
hide_sort = 'true' if dt == 'series' else 'false'
|
||||||
if category == 'search':
|
if category == 'search':
|
||||||
which = unhexlify(cid)
|
which = unhexlify(cid).decode('utf-8')
|
||||||
try:
|
try:
|
||||||
ids = self.search_cache('search:"%s"'%which)
|
ids = self.search_cache('search:"%s"'%which)
|
||||||
except:
|
except:
|
||||||
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
|
raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
|
||||||
elif category == 'newest':
|
elif category == 'newest':
|
||||||
ids = list(self.db.data.iterallids())
|
ids = self.search_cache('')
|
||||||
hide_sort = 'true'
|
hide_sort = 'true'
|
||||||
|
elif category == 'allbooks':
|
||||||
|
ids = self.search_cache('')
|
||||||
else:
|
else:
|
||||||
q = category
|
q = category
|
||||||
if q == 'news':
|
if q == 'news':
|
||||||
@ -512,7 +538,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,
|
||||||
@ -560,17 +587,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)
|
||||||
@ -583,7 +611,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']
|
||||||
@ -608,8 +637,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
|
||||||
@ -628,7 +658,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]
|
||||||
@ -684,7 +715,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)
|
||||||
|
@ -10,7 +10,10 @@ from calibre.utils.ordered_dict import OrderedDict
|
|||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
|
|
||||||
def add_routes(self, c):
|
def __init__(self):
|
||||||
|
self.reset_caches()
|
||||||
|
|
||||||
|
def reset_caches(self):
|
||||||
self._category_cache = OrderedDict()
|
self._category_cache = OrderedDict()
|
||||||
self._search_cache = OrderedDict()
|
self._search_cache = OrderedDict()
|
||||||
|
|
||||||
|
@ -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'
|
||||||
@ -124,10 +128,11 @@ class ContentServer(object):
|
|||||||
if want_mobile:
|
if want_mobile:
|
||||||
return self.mobile()
|
return self.mobile()
|
||||||
|
|
||||||
return self.static('index.html')
|
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):
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from calibre.library.server import server_config as config
|
from calibre.library.server import server_config as config
|
||||||
@ -25,7 +25,18 @@ def stop_threaded_server(server):
|
|||||||
server.thread = None
|
server.thread = None
|
||||||
|
|
||||||
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,
|
||||||
@ -38,50 +49,18 @@ def option_parser():
|
|||||||
' in the GUI'))
|
' in the GUI'))
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|
||||||
try:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid > 0:
|
|
||||||
# exit first parent
|
|
||||||
sys.exit(0)
|
|
||||||
except OSError, e:
|
|
||||||
print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# decouple from parent environment
|
|
||||||
os.chdir("/")
|
|
||||||
os.setsid()
|
|
||||||
os.umask(0)
|
|
||||||
|
|
||||||
# do second fork
|
|
||||||
try:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid > 0:
|
|
||||||
# exit from second parent
|
|
||||||
sys.exit(0)
|
|
||||||
except OSError, e:
|
|
||||||
print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Redirect standard file descriptors.
|
|
||||||
si = file(stdin, 'r')
|
|
||||||
so = file(stdout, 'a+')
|
|
||||||
se = file(stderr, 'a+', 0)
|
|
||||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
|
||||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
|
||||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv):
|
def main(args=sys.argv):
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
opts, args = parser.parse_args(args)
|
opts, args = parser.parse_args(args)
|
||||||
if opts.daemonize and not iswindows:
|
if opts.daemonize and not iswindows:
|
||||||
daemonize()
|
from cherrypy.process.plugins import Daemonizer
|
||||||
|
d = Daemonizer(cherrypy.engine)
|
||||||
|
d.subscribe()
|
||||||
if opts.pidfile is not None:
|
if opts.pidfile is not None:
|
||||||
with open(opts.pidfile, 'wb') as f:
|
from cherrypy.process.plugins import PIDFile
|
||||||
f.write(str(os.getpid()))
|
PIDFile(cherrypy.engine, opts.pidfile).subscribe()
|
||||||
cherrypy.log.screen = True
|
cherrypy.log.screen = True
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
if opts.with_library is None:
|
if opts.with_library is 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,26 +108,28 @@ 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'))
|
||||||
|
|
||||||
data = TD()
|
data = TD()
|
||||||
last = None
|
|
||||||
for fmt in book['formats'].split(','):
|
for fmt in book['formats'].split(','):
|
||||||
a = ascii_filename(book['authors'])
|
a = ascii_filename(book['authors'])
|
||||||
t = ascii_filename(book['title'])
|
t = ascii_filename(book['title'])
|
||||||
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)
|
||||||
),
|
),
|
||||||
CLASS('button'))
|
CLASS('button'))
|
||||||
s.tail = u''
|
s.tail = u''
|
||||||
last = s
|
|
||||||
data.append(s)
|
data.append(s)
|
||||||
|
|
||||||
|
div = DIV(CLASS('data-container'))
|
||||||
|
data.append(div)
|
||||||
|
|
||||||
series = u'[%s - %s]'%(book['series'], book['series_index']) \
|
series = u'[%s - %s]'%(book['series'], book['series_index']) \
|
||||||
if book['series'] else ''
|
if book['series'] else ''
|
||||||
tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
|
tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
|
||||||
@ -137,13 +140,13 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
|||||||
if val:
|
if val:
|
||||||
ctext += '%s=[%s] '%tuple(val.split(':#:'))
|
ctext += '%s=[%s] '%tuple(val.split(':#:'))
|
||||||
|
|
||||||
text = u'\u202f%s %s by %s - %s - %s %s %s' % (book['title'], series,
|
first = SPAN(u'\u202f%s %s by %s' % (book['title'], series,
|
||||||
book['authors'], book['size'], book['timestamp'], tags, ctext)
|
book['authors']), CLASS('first-line'))
|
||||||
|
div.append(first)
|
||||||
if last is None:
|
second = SPAN(u'%s - %s %s %s' % ( book['size'],
|
||||||
data.text = text
|
book['timestamp'],
|
||||||
else:
|
tags, ctext), CLASS('second-line'))
|
||||||
last.tail += text
|
div.append(second)
|
||||||
|
|
||||||
bookt.append(TR(thumbnail, data))
|
bookt.append(TR(thumbnail, data))
|
||||||
# }}}
|
# }}}
|
||||||
@ -153,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
|
|||||||
TITLE(__appname__ + ' Library'),
|
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
|
||||||
@ -173,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'):
|
||||||
@ -229,7 +234,7 @@ class MobileServer(object):
|
|||||||
no_tag_count=True)
|
no_tag_count=True)
|
||||||
book['title'] = record[FM['title']]
|
book['title'] = record[FM['title']]
|
||||||
for x in ('timestamp', 'pubdate'):
|
for x in ('timestamp', 'pubdate'):
|
||||||
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
book[x] = strftime('%b, %Y', record[FM[x]])
|
||||||
book['id'] = record[FM['id']]
|
book['id'] = record[FM['id']]
|
||||||
books.append(book)
|
books.append(book)
|
||||||
for key in CKEYS:
|
for key in CKEYS:
|
||||||
@ -258,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.
|
||||||
|
|
||||||
|
@ -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
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user