Sync to trunk.
12
COPYRIGHT
@ -30,6 +30,12 @@ License: BSD
|
||||
The full text of the BSD license is distributed as in
|
||||
/usr/share/common-licenses/BSD on Debian systems.
|
||||
|
||||
Files: /src/routes/*
|
||||
Copyright: Copyright (c) 2005-2008 Ben Bangert <ben@groovie.org>
|
||||
License: BSD
|
||||
The full text of the BSD license is distributed as in
|
||||
/usr/share/common-licenses/BSD on Debian systems.
|
||||
|
||||
Files: src/odf/*
|
||||
Copyright: Copyright (C) 2006-2008 Søren Roug, European Environment Agency
|
||||
License: LGPL2.1+
|
||||
@ -50,12 +56,6 @@ License: BSD
|
||||
The full text of the BSD license is distributed as in
|
||||
/usr/share/common-licenses/BSD on Debian systems.
|
||||
|
||||
Files: src/calibre/utils/genshi/*
|
||||
Copyright: Copyright (C) 2006-2008 Edgewall Software
|
||||
License: BSD
|
||||
The full text of the BSD license is distributed as in
|
||||
/usr/share/common-licenses/BSD on Debian systems.
|
||||
|
||||
Files: src/calibre/utils/lzx/*
|
||||
Copyright: Copyright (C) 2002, Matthew T. Russotto
|
||||
Copyright: Copyright (C) 2008, Marshall T. Vandegrift <llasram@gmail.com>
|
||||
|
@ -4,6 +4,13 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.0
|
||||
date: 2010-06-04
|
||||
|
||||
new features:
|
||||
- title: "Go to http://calibre-ebook.com/new-in/seven to see what's new in 0.7.0"
|
||||
type: major
|
||||
|
||||
- version: 0.6.55
|
||||
date: 2010-05-28
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 57 KiB |
@ -123,7 +123,7 @@ function fetch_library_books(start, num, timeout, sort, order, search) {
|
||||
|
||||
current_library_request = $.ajax({
|
||||
type: "GET",
|
||||
url: "library",
|
||||
url: "xml",
|
||||
data: data,
|
||||
cache: false,
|
||||
timeout: timeout, //milliseconds
|
||||
|
@ -11,7 +11,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="banner">
|
||||
<a style="border: 0pt" href="http://calibre-ebook.com" alt="calibre" title="calibre"><img style="border:0pt" src="/static/calibre.png" alt="calibre" /></a>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div id="search_box">
|
||||
|
@ -25,3 +25,39 @@ series_index_auto_increment = 'next'
|
||||
# copy : copy author to author_sort without modification
|
||||
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
||||
author_sort_copy_method = 'invert'
|
||||
|
||||
|
||||
# Set whether boolean custom columns are two- or three-valued.
|
||||
# Two-values for true booleans
|
||||
# three-values for yes/no/unknown
|
||||
# Set to 'yes' for three-values, 'no' for two-values
|
||||
bool_custom_columns_are_tristate = 'yes'
|
||||
|
||||
|
||||
# Provide a set of columns to be sorted on when calibre starts
|
||||
# The argument is None if saved sort history is to be used
|
||||
# otherwise it is a list of column,order pairs. Column is the
|
||||
# lookup/search name, found using the tooltip for the column
|
||||
# Order is 0 for ascending, 1 for descending
|
||||
# For example, set it to [('authors',0),('title',0)] to sort by
|
||||
# title within authors.
|
||||
sort_columns_at_startup = None
|
||||
|
||||
# Format to be used for publication date
|
||||
# A string controlling how the publication date is displayed in the GUI
|
||||
# d the day as number without a leading zero (1 to 31)
|
||||
# dd the day as number with a leading zero (01 to 31)
|
||||
# ddd the abbreviated localized day name (e.g. 'Mon' to 'Sun').
|
||||
# dddd the long localized day name (e.g. 'Monday' to 'Qt::Sunday').
|
||||
# M the month as number without a leading zero (1-12)
|
||||
# MM the month as number with a leading zero (01-12)
|
||||
# MMM the abbreviated localized month name (e.g. 'Jan' to 'Dec').
|
||||
# MMMM the long localized month name (e.g. 'January' to 'December').
|
||||
# yy the year as two digit number (00-99)
|
||||
# yyyy the year as four digit number
|
||||
# For example, given the date of 9 Jan 2010, the following formats show
|
||||
# MMM yyyy ==> Jan 2010 yyyy ==> 2010 dd MMM yyyy ==> 09 Jan 2010
|
||||
# MM/yyyy ==> 01/2010 d/M/yy ==> 9/1/10 yy ==> 10
|
||||
# default if not set: MMM yyyy
|
||||
gui_pubdate_display_format = 'MMM yyyy'
|
||||
|
||||
|
63
resources/images/blank.svg
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="500"
|
||||
height="500"
|
||||
id="svg3152"
|
||||
version="1.1"
|
||||
inkscape:version="0.47 r22583"
|
||||
sodipodi:docname="New document 1">
|
||||
<defs
|
||||
id="defs3154">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective3160" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="1.0"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:zoom="0.34"
|
||||
inkscape:cx="350"
|
||||
inkscape:cy="520"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="643"
|
||||
inkscape:window-height="666"
|
||||
inkscape:window-x="125"
|
||||
inkscape:window-y="125"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata3157">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-552.36218)" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
61
resources/images/column.svg
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) --><svg height="253.5" id="Layer_1" inkscape:version="0.40+cvs" sodipodi:docbase="F:\openclip\svg3" sodipodi:docname="Capitello modanatura moulure.svg" sodipodi:version="0.32" style="overflow:visible;enable-background:new 0 0 277.433 253.5;" version="1.0" viewBox="0 0 277.433 253.5" width="277.433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xml="http://www.w3.org/XML/1998/namespace"><metadata><rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><cc:Work rdf:about=""><dc:title>Capitello modanatura modanature moulure moulures</dc:title><dc:description></dc:description><dc:subject><rdf:Bag><rdf:li>building</rdf:li></rdf:Bag></dc:subject><dc:publisher><cc:Agent rdf:about="http://www.openclipart.org"><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:publisher><dc:creator><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:creator><dc:rights><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:rights><dc:date></dc:date><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://web.resource.org/cc/PublicDomain"/><dc:language>en</dc:language></cc:Work><cc:License rdf:about="http://web.resource.org/cc/PublicDomain"><cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/><cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/><cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/></cc:License></rdf:RDF></metadata>
|
||||
<defs id="defs56"></defs>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="Layer_1" inkscape:cx="138.71651" inkscape:cy="126.75000" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="540" inkscape:window-width="640" inkscape:window-x="22" inkscape:window-y="22" inkscape:zoom="1.4911243" pagecolor="#ffffff"></sodipodi:namedview>
|
||||
|
||||
<g id="g3">
|
||||
<path d="M269.987,16.446c11.514,10.547,7.726,33.31,1.663,45.651c-9.559,19.459-32.822,21.546-51.841,19.009 c-2.021,22.636-2.369,45.377-2.399,68.09c-0.015,10.891,0.025,21.781,0.025,32.672c0,5.381,0,10.762,0,16.143 c0,4.35,1.145,5.354,5.544,5.098c2.975-0.172,12.557-3.239,12.893,1.706c0.221,3.245-2.241,9.014-0.016,11.713 c1.997,2.421,5.807,3.181,8.67,3.993c3.729,1.058,7.776,3.027,11.667,3.265c2.984,0.182,1.435,5.441,1.368,7.535 c-0.148,4.68,0.078,9.268,0.319,13.928c0.24,4.638-1.359,4.189-5.543,4.594c-21.966,2.125-44.538,0.341-66.583,0.291 c-22.807-0.052-45.619,0.078-68.409,1.014c-22.082,0.906-44.168,0.835-66.252,1.523c-9,0.28-18.09,0.771-27.071,0.831 c-3.313,0.022-3.382-1.347-2.935-4.079c0.726-4.434,0.467-8.914,0.467-13.398c0-2.373-1.074-7.706,1.34-9.398 c2.938-2.061,7.151-2.742,10.401-4.265c9.135-4.276,6.441-7.791,6.591-16.34c0.073-4.137,5.426-1.096,8.395-1.154 c4.361-0.087,2.593-4.402,2.559-7.859c-0.064-6.333-0.206-12.665-0.382-18.995c-0.639-23.031-1.182-46.073-2.019-69.097 c-0.196-5.4-0.352-10.801-0.472-16.204c-0.058-2.589,1.452-11.419-2.132-11.275c-8.722,0.349-17.113-0.684-25.05-4.56 C-12.651,60.547-3.602,8.372,32.616,2.474c9.309-1.516,19.017-0.612,28.399-0.511c11.773,0.126,23.546,0.163,35.32,0.157 c23.119-0.013,46.238-0.115,69.354-0.556C185.921,1.177,206.175-0.019,226.411,0C242.655,0.015,259.596,3.013,269.987,16.446" id="path5"></path>
|
||||
|
||||
<path d="M263.497,12.805c17.229,17.231,13.408,52.826-9.883,63.111c-11.111,4.906-29.468,6.652-39.829-0.784 c-4.768-3.42-10.636-8.188-13.193-13.588c-2.79-5.893-0.708-14.659,1.955-20.272c5.144-10.839,15.963-19.15,28.248-18.245 c13.067,0.963,21.35,10.229,19.214,23.444c-1.684,10.42-12.965,22.841-24.252,15.978c-4.984-3.032-8.286-9.521-6.396-15.307 c1.08-3.303,8.425-8.779,10.105-2.996c-3.617-0.933-3.468,5.562-2.88,7.464c1.694,5.481,8.471,5.282,12.069,1.689 c10.924-10.907-4.807-28.457-17.827-22.066c-15.255,7.487-15.464,27.982-1.153,36.475c13.494,8.008,31.806,1.371,39.313-11.699 c6.751-11.753,4.543-29.552-5.98-38.545c-6.412-5.48-16.133-5.78-24.13-6.6C217.514,9.7,206.07,9.611,194.659,9.88 c-45.654,1.077-91.252,2.635-136.93,2.865c-10.516,0.053-26.706-3.068-35.7,3.59C13.072,22.966,7.624,34.89,8.907,45.965 c2.465,21.27,26.852,34.439,45.764,23.854C63.05,65.13,69.272,56.151,68.154,46.26c-1.048-9.266-8.71-17.296-18.334-17.278 c-10.437,0.02-18.804,10.469-13.622,20.189c2.062,3.867,8.494,5.687,11.636,1.998c2.241-2.63,1.254-7.959-2.931-7.419 c4.833-5.223,11.914,1.513,11.38,7.142c-0.623,6.566-7.648,11.135-13.593,12.024c-15.159,2.267-23.777-11.625-20.532-25.271 c3.998-16.809,21.359-21.542,36.573-17.014c17.902,5.329,19.484,20.76,13.54,36.295C59.813,89.49,7.194,84.666,2.164,49.499 c-2.4-16.776,6.42-36.324,22.822-42.628c7.058-2.713,14.77-2.768,22.225-2.692C58.154,4.29,69.098,4.427,80.042,4.491 c32.615,0.19,65.303,0.147,97.908-0.728c16.363-0.439,32.709-1.273,49.082-0.806C239.347,3.309,254.146,3.507,263.497,12.805" id="path7" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M258.749,31.246c3.035,11.892-0.551,27.027-11.674,33.584c-10.348,6.101-28.37,4.505-33.847-7.551 c-4.545-10.002,2.436-25.982,14.791-24.727c6.887,0.7,10.927,5.838,10.588,12.623c-0.167,3.326-0.945,6.588-4.631,7.439 c-3.448,0.796-6.992-1.519-4.906-5.223c4.045,5.535,5.103-6.75-1.358-8.312c-6.246-1.511-10.79,3.854-10.992,9.564 c-0.493,13.941,14.36,23.351,26.353,15.28c11.628-7.826,13.93-28.233,3.285-37.841c-6.293-5.681-15.592-6.508-23.673-6.923 c-11.101-0.57-22.227,0.536-33.31,1.041c-22.333,1.019-44.661,2.149-66.996,3.14c-10.522,0.466-21.048,0.829-31.577,1.124 c-5.074,0.142-10.148,0.27-15.223,0.378c-5.138,0.109-9.622-3.251-14.107-5.271c-13.721-6.181-33.438-2.347-40.341,11.952 c-5.646,11.696-1.719,30.13,12.218,33.742c10.756,2.788,31.213-5.459,25.068-19.708c-2.384-5.527-9.835-7.564-14.897-4.53 c-1.53,0.917-5.698,8.159-1.748,6.881c1.042-0.337,1.828-1.721,3.053-1.544c2.031,0.294,1.867,1.657,0.453,2.672 c-6.196,4.449-9.433-4.01-7.865-8.951c2.52-7.944,13.175-10.17,19.805-6.519c13.573,7.474,8.818,25.74-2.296,32.597 c-14.444,8.912-34.788,3.045-41.537-12.791C7.439,39.422,15.236,19.093,30.86,15.584c9.506-2.136,20.457-0.466,30.129-0.384 c12.077,0.103,24.155,0.117,36.232,0.044c23.102-0.14,46.172-0.626,69.248-1.748c19.153-0.93,38.758-2.352,57.946-1.18 C237.174,13.095,255.954,15.957,258.749,31.246" id="path9" style="fill:#808080;"></path>
|
||||
|
||||
<path d="M216.96,22.065c-4.189,1.457-6.926,5.1-10.035,8.035c-3.926,3.706-7.758,3.477-12.894,3.057 c-13.75-1.124-27.989-0.009-41.786,0.081c-14.754,0.095-29.439,0.667-44.163,1.609c-7.355,0.471-14.71,0.961-22.071,1.337 c-3.388,0.173-7.721,1.855-8.424-1.65c-0.532-2.656-2.151-4.901-3.401-7.245c25.986-0.478,51.909-1.521,77.857-2.958 c12.748-0.706,25.585-1.691,38.35-1.883c5.152-0.078,10.293,0.221,15.423-0.352C209.098,21.729,213.818,20.556,216.96,22.065" id="path11" style="fill:#5E5E5E;"></path>
|
||||
|
||||
<path d="M202.556,35.282c-3.06,6.708-6.111,13.132-5.917,20.674c0.065,2.534,0.354,5.179,1.679,7.404 c1.462,2.459,4.185,3.993,4.92,6.893c1.413,5.582-0.519,12.133-0.678,17.817c-0.233,8.271-0.503,16.542-0.691,24.813 c-0.373,16.291-0.483,32.586-0.646,48.88c-0.081,8.149-0.175,16.3-0.321,24.449c-0.058,3.206,1.931,16.342-2.582,17.381 c-4.207,0.97-9.048,1.308-9.19-3.83c-0.206-7.431,0.059-14.804,0.187-22.237c0.296-17.278,0.149-34.562,0.442-51.841 c0.278-16.383,0.703-32.763,1.037-49.146c0.156-7.656,0.222-15.316,0.411-22.972c0.161-6.488-5.237-11.803-7.485-17.813 C189.794,33.726,196.337,35.792,202.556,35.282" id="path13" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M149.926,35.678c-1.179,5.098-7.83,6.24-8.979,10.95c-1.648,6.761-0.583,15.063-0.745,22.007 c-0.406,17.411-0.884,34.822-0.57,52.239c0.308,17.057,0.717,34.095,0.717,51.156c0,8.01,0,16.02,0,24.028 c0,7.207,0.133,7.996-7.093,7.5c-6.914-0.475-3.747-15.828-3.729-20.809c0.059-16.925,0.119-33.849,0.178-50.773 c0.061-17.404-0.096-34.828,0.439-52.226c0.247-8.041,0.696-16.035,0.593-24.084c-0.11-8.56-2.827-11.851-8.037-18.326 C131.735,36.181,140.884,36.717,149.926,35.678" id="path15" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M180.872,35.678c-0.979,4.869-5.544,7.579-8.596,11.063c-4.358,4.976-2.346,12.479-2.414,18.523 c-0.192,17.04-1.516,33.989-1.317,51.069c0.191,16.582,0.597,33.16,0.705,49.742c0.054,8.282,0.034,16.565-0.123,24.847 c-0.069,3.652,0.211,7.544-0.281,11.169c-0.41,3.021-5.537,2.033-7.736,2.147c-1.793,0.094-1.443-80.056-1.404-87.238 c0.085-16.094,0.108-32.21,0.636-48.297c0.19-5.829,0.481-11.56,1.162-17.345c0.758-6.434-3.308-10.796-7.856-15.206 c3.301-1.833,8.131-0.664,11.682-0.447C170.432,36.017,175.776,36.123,180.872,35.678" id="path17" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M118.269,37.735c-1.283,2.154-2.983,4.106-4.986,5.619c-1.565,1.183-3.853,1.641-4.952,3.395 c-1.47,2.347,0.599,5.242,0.599,7.685c0,4.216-0.604,8.428-0.717,12.647c-0.438,16.338-0.463,32.68-0.272,49.021 c0.194,16.553,0.592,33.098,1.683,49.619c0.389,5.881,5.109,37.916-2.616,38.886c-3.479,0.438-8.817,1.344-9.113-3.157 c-0.499-7.577-0.5-15.216-0.751-22.806c-0.55-16.556-1.148-33.111-1.437-49.675c-0.284-16.31-0.281-32.628,0.23-48.934 c0.255-8.16,0.637-16.315,1.172-24.461c0.475-7.23-3.972-10.534-5.669-17.048C100.385,38.349,109.315,37.358,118.269,37.735" id="path19" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M158.395,46.125c-0.533,31.886-1.188,63.758-1.188,95.65c0,15.317,0,30.634,0,45.95 c0,4.617,0.757,9.24,0.664,13.833c-0.068,3.371-8.199,3.735-8.344,0.423c-1.413-32.226-1.844-64.548-1.556-96.803 c0.143-15.907,0.771-31.809,1.218-47.709c0.135-4.806-1.466-13.862,1.021-18.2C152.52,35.243,157.134,45.118,158.395,46.125" id="path21" style="fill:#969696;"></path>
|
||||
|
||||
<path d="M86.137,40.11c-1.802,4.444-6.351,6.834-8.785,10.843c0.118-3.233-1.075-7.139,0.521-10.162 C78.82,38.997,86.6,37.518,86.137,40.11" id="path23" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M127.924,47.708c-0.864,29.137-0.867,58.291-0.963,87.438c-0.048,14.845-0.097,29.689-0.146,44.534 c-0.023,6.938,0.175,13.919-0.089,20.853c-0.133,3.509-2.079,3.183-5.074,3.256c-3.186,0.077-1.998-5.488-2.133-8.231 c-0.182-3.69-0.362-7.381-0.538-11.071c-0.351-7.383-0.684-14.767-0.974-22.152c-0.581-14.771-0.985-29.548-1.057-44.33 c-0.071-14.73,0.919-29.391,1.103-44.1c0.088-7.041-0.18-14.086-0.18-21.128c0-5.268-1.026-9.582,2.85-13.457 C123.231,42.02,125.295,45.115,127.924,47.708" id="path25" style="fill:#969696;"></path>
|
||||
|
||||
<path d="M188.469,83.005c-0.868,27.782-1.484,55.573-1.786,83.367c-0.124,11.389,0.632,23.096-0.191,34.44 c-0.213,2.944-4.001,4.863-6.649,2.729c-2.821-2.274-2.137-11.711-2.193-14.737c-0.244-12.991,0.978-25.921,0.54-38.927 c-0.438-12.993-0.009-25.956,0.292-38.947c0.301-13.003,0.601-26.007,0.907-39.01c0.25-10.571-2.265-22.762,2.749-32.602 C192.517,51.294,189.273,68.576,188.469,83.005" id="path27" style="fill:#969696;"></path>
|
||||
|
||||
<path d="M95.001,51.744c-1.75,28.572-2.194,57.212-1.738,85.831c0.216,13.561,0.746,27.112,1.23,40.665 c0.248,6.934,0.492,13.867,0.735,20.801c0.229,6.533-2.363,4.685-7.43,5.211c-1.578-28.564-2.779-57.147-2.978-85.757 c-0.093-13.326-0.059-26.656,0.183-39.98c0.121-6.69,0.447-13.376,0.565-20.066c0.106-6.027,0.059-12.19,3.418-17.468 C90.991,44.568,92.996,48.156,95.001,51.744" id="path29" style="fill:#969696;"></path>
|
||||
|
||||
<path d="M145.177,87.437c0.988,25.058,1.155,50.139,1.183,75.214c0.013,11.358,0.005,22.718,0.005,34.077 c0,1.782,0.651,4.979-0.428,6.579c-2.152,3.19-2.427-2.293-2.446-2.981c-0.175-6.101-0.312-12.202-0.37-18.306 c-0.233-24.164-0.411-48.273-0.979-72.433c-0.275-11.732,0.069-23.448,0.522-35.172c0.243-6.293,0.513-12.586,0.71-18.882 c0.128-4.116-0.891-8.615,2.99-11.388C146.64,58.581,146.323,73.043,145.177,87.437" id="path31" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M115.42,50.953c-1.174,28.427-1.397,56.849-0.762,85.292c0.306,13.686,0.66,27.37,1.144,41.051 c0.247,6.976,0.493,13.949,0.709,20.926c0.113,3.651,0.938,5.553-3.386,6.031c-0.947-29.533-2.22-59.063-2.748-88.608 c-0.248-13.869-0.502-27.894,0.314-41.748c0.369-6.252,1.427-12.552,0.738-18.821c-0.473-4.301-1.087-7.463,3.278-9.742 C114.97,47.199,115.34,49.065,115.42,50.953" id="path33" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M177.626,47.312c-1.061,29.439-2.265,58.83-1.941,88.295c0.16,14.534-0.268,29.064-0.391,43.597 c-0.057,6.632-0.034,13.265,0.191,19.893c0.047,1.372,0.716,3.423-0.136,4.674c-0.996,1.462-2.972,0.332-3.359-0.947 c-1.224-4.041-0.131-9.819-0.212-13.987c-0.144-7.308-0.277-14.616-0.389-21.924c-0.224-14.616-0.359-29.235-0.309-43.853 c0.051-14.634,0.195-29.277,0.585-43.906c0.269-10.124-2.24-25.542,5.248-33.028C177.136,46.464,177.512,46.979,177.626,47.312" id="path35" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M82.18,85.063c0.151,25.021-0.078,50.047,0.583,75.063c0.338,12.787,1.207,25.561,1.396,38.35 c0.032,2.203,1.505,6.316-2.127,5.501c-1.73-0.388-1.261-14.663-1.35-16.759c-1.034-24.321-2.365-48.646-2.663-72.991 c-0.149-12.221-0.477-24.412,0.259-36.618c0.576-9.556-1.54-21.67,4.694-29.426C82.115,60.46,82.981,72.783,82.18,85.063" id="path37" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M76.56,148.457c0.157,10.251,0.532,20.496,0.961,30.738c0.296,7.057,2.459,16.297,1.102,23.311 c-0.727,3.755-9.991,3.729-10.081-0.096c-0.255-10.852-0.904-21.675-1.379-32.518c-0.936-21.354-1.054-42.732-1.057-64.104 c-0.001-9.405-0.544-18.909-0.314-28.295c0.123-5.044,2.847-7.453,5.167-11.591c2.299-4.102,3.548-8.677,5.603-12.893 C75.467,84.813,75.458,116.654,76.56,148.457" id="path39" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M207.781,73.825c0.317,21.825-0.792,43.493-0.792,65.278c0,10.738,0,21.478,0,32.216 c0,5.481,0,10.963,0,16.445c0,2.65,1.686,12.848-0.612,14.655c-4.082,3.21-2.532-8.896-2.516-10.782 c0.052-5.517,0.092-11.034,0.129-16.552c0.074-11.035,0.133-22.071,0.23-33.106c0.202-22.987,0.569-45.978,1.582-68.945 C206.461,73.297,207.122,73.561,207.781,73.825" id="path41" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M63.66,154.472c0.783,16.721,1.787,33.437,2.058,50.177c-2.401-0.132-4.802-0.265-7.203-0.396 c4.111-2.813,0.355-17.984,0.207-22.342c-0.486-14.264-0.922-28.527-1.294-42.794c-0.371-14.189-1.13-28.355-1.666-42.537 c-0.121-3.195-2.525-15.268-0.633-17.566c1.9-2.309,5.626-3.449,8.214-4.792c0.066,13.375,0.132,26.75,0.198,40.125 C63.607,127.713,64.891,141.124,63.66,154.472" id="path43" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M216.96,80.235c0.446,21.664-0.711,43.313-1.313,64.962c-0.296,10.651-0.357,21.305-0.45,31.959 c-0.03,3.503,1.627,25.193-1.97,25.515c-5.911,0.527-3.371-20.418-3.351-24.031c0.063-10.85,0.132-21.698,0.181-32.548 c0.104-23.14,0.117-46.282,0.571-69.418C213.076,77.254,214.519,79.697,216.96,80.235" id="path45" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M52.421,79.84c0.88,22.406,1.916,44.805,2.742,67.213c0.432,11.725,0.802,23.45,1.042,35.18 c0.088,4.317-2.453,18.714,1.124,22.02c-4.652,0-3.771-2.246-3.999-6.369c-0.426-7.703-0.767-15.425-0.831-23.14 c-0.122-14.887-0.063-29.809-0.743-44.682C51.631,127.329,46.842,79.502,52.421,79.84" id="path47" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M233.027,205.044c2.539,9.07-2.377,10.524-9.861,11.079c-9.534,0.707-19.151,0.729-28.707,0.77 c-20.313,0.088-40.627,0.037-60.94,0.133c-19.374,0.091-38.575,1.29-57.922,1.899c-9.106,0.287-18.38,0.342-27.438-0.75 c-3.971-0.479-5.018,0.146-5.204-3.817c-0.107-2.286-0.019-4.574-0.109-6.859c31.892,0,63.831-1.94,95.708-1.301 c16.213,0.325,32.394,0.372,48.607,0.431C202.487,206.683,217.678,205.044,233.027,205.044" id="path49" style="fill:#BFBFBF;"></path>
|
||||
|
||||
<path d="M249.092,224.275c-37.66,2.193-75.668-0.496-113.257,2.421c-36.526,2.834-73.583,2.146-110.243,1.22 c14.839-9.382,31.379-6.451,47.913-6.075c20.089,0.457,40.228-2.287,60.35-2.391c20.478-0.105,40.953,0.454,61.432,0.33 c10.018-0.061,19.98-0.497,29.972-1.229C234.292,217.891,240.738,221.712,249.092,224.275" id="path51" style="fill:#E3E3E3;"></path>
|
||||
|
||||
<path d="M255.108,245.961c-1.281,0.09-1.54,1.658-2.793,1.662c-2.189,0.008-4.379,0.015-6.568,0.022 c-5.926,0.021-11.852,0.04-17.777,0.061c-11.854,0.041-23.707,0.08-35.561,0.121c-23.952,0.081-47.894,0.144-71.839,0.773 c-23.992,0.63-47.979,1.343-71.98,1.604c-6.661,0.072-13.326,0.174-19.988,0.186c-3.327,0.005-4.571,1.054-4.644-2.605 c-0.115-5.806-0.229-11.61-0.344-17.415c23.308,2.299,47.056,0.97,70.449,1.107c23.82,0.141,47.644-2.438,71.469-3.03 c23.765-0.591,47.532-0.933,71.303-0.928c3.688,0.001,17.249-3.522,18.395,1.103C256.617,234.223,255.108,240.288,255.108,245.961" id="path53" style="fill:#BFBFBF;"></path>
|
||||
|
||||
</g>
|
||||
|
||||
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 16 KiB |
553
resources/images/devices/folder.svg
Normal file
@ -0,0 +1,553 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
x="0.0000000"
|
||||
y="0.0000000"
|
||||
width="48.000000px"
|
||||
height="48.000000px"
|
||||
id="svg1"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.44"
|
||||
sodipodi:docname="folder.svg"
|
||||
sodipodi:docbase="/home/lapo/Icone/Crux/crux-icon-theme/scalable/places"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-daritaliare.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape">
|
||||
<metadata
|
||||
id="metadata162">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Folder</dc:title>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>Lapo Calamandrei</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:date>2006-06-26</dc:date>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
|
||||
<dc:identifier />
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>folder</rdf:li>
|
||||
<rdf:li>directory</rdf:li>
|
||||
<rdf:li>storage</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/Notice" />
|
||||
<cc:permits
|
||||
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/ShareAlike" />
|
||||
<cc:requires
|
||||
rdf:resource="http://web.resource.org/cc/SourceCode" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666"
|
||||
borderopacity="1"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1041"
|
||||
inkscape:window-height="655"
|
||||
inkscape:cy="24.626698"
|
||||
inkscape:cx="45.136759"
|
||||
inkscape:zoom="8"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:window-x="504"
|
||||
inkscape:window-y="101"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:showpageshadow="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:object-paths="false"
|
||||
gridspacingx="0.5px"
|
||||
gridspacingy="0.5px"
|
||||
gridempspacing="2"
|
||||
inkscape:grid-points="false"
|
||||
showborder="true"
|
||||
borderlayer="true">
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="36.062446"
|
||||
id="guide1934" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="15.003922"
|
||||
id="guide1941" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="4.5"
|
||||
id="guide1943" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="44.503533"
|
||||
id="guide1945" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="43.125"
|
||||
id="guide1947" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="39"
|
||||
id="guide1949" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="19.003495"
|
||||
id="guide2919" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="0.97227183"
|
||||
id="guide2212" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="47.994873"
|
||||
id="guide2214" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="111"
|
||||
id="guide4328" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="65.75"
|
||||
id="guide3135" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="129.75"
|
||||
id="guide3137" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="190.75"
|
||||
id="guide3139" />
|
||||
<sodipodi:guide
|
||||
orientation="vertical"
|
||||
position="212.48559"
|
||||
id="guide3316" />
|
||||
<sodipodi:guide
|
||||
orientation="horizontal"
|
||||
position="178.01413"
|
||||
id="guide3318" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4232">
|
||||
<stop
|
||||
style="stop-color:#ad7fa8;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4234" />
|
||||
<stop
|
||||
style="stop-color:#75507b;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4236" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3311">
|
||||
<stop
|
||||
style="stop-color:#888a85;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3313" />
|
||||
<stop
|
||||
style="stop-color:#555753;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop3315" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3076">
|
||||
<stop
|
||||
id="stop3078"
|
||||
offset="0"
|
||||
style="stop-color:#5c3566;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop3080"
|
||||
offset="1"
|
||||
style="stop-color:#5c3566;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2994">
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2996" />
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2998" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3923">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3925" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3927" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3908">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3910" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3912" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2894">
|
||||
<stop
|
||||
style="stop-color:#39213f;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2896" />
|
||||
<stop
|
||||
id="stop2900"
|
||||
offset="0.47619048"
|
||||
style="stop-color:#75507b;stop-opacity:1;" />
|
||||
<stop
|
||||
style="stop-color:#5c3566;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2898" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3100">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3102" />
|
||||
<stop
|
||||
id="stop2071"
|
||||
offset="1"
|
||||
style="stop-color:white;stop-opacity:0;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3018">
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3020" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3022" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3100"
|
||||
id="linearGradient3685"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="17.02047"
|
||||
y1="-16.276186"
|
||||
x2="17.02047"
|
||||
y2="-29.344501" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2894"
|
||||
id="linearGradient3687"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1,0,0,0.996152,0,85.74795)"
|
||||
x1="9.4176369"
|
||||
y1="-44.922661"
|
||||
x2="9.4176369"
|
||||
y2="-59.636772" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3076"
|
||||
id="linearGradient3689"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="16.749592"
|
||||
y1="21.616077"
|
||||
x2="16.749592"
|
||||
y2="32.797989" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2994"
|
||||
id="linearGradient3691"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="16.749592"
|
||||
y1="40.51022"
|
||||
x2="16.749592"
|
||||
y2="36.268337" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3908"
|
||||
id="linearGradient3693"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="8.25"
|
||||
y1="-14.375"
|
||||
x2="8.25"
|
||||
y2="-30.879261" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3923"
|
||||
id="radialGradient3695"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(2.268741,0,-1.646661e-6,0.184077,-10.86781,18.45272)"
|
||||
cx="10.189716"
|
||||
cy="16.554359"
|
||||
fx="10.189716"
|
||||
fy="16.554359"
|
||||
r="22.5" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3311"
|
||||
id="linearGradient3938"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,1)"
|
||||
x1="20.625"
|
||||
y1="-90.064087"
|
||||
x2="20.625"
|
||||
y2="-84.029831" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3018"
|
||||
id="linearGradient3129"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="9"
|
||||
y1="-92.805496"
|
||||
x2="9"
|
||||
y2="-83.4375" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4232"
|
||||
id="linearGradient4238"
|
||||
x1="0.99999888"
|
||||
y1="30.499076"
|
||||
x2="53.999733"
|
||||
y2="37.624077"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="vectors"
|
||||
style="display:inline">
|
||||
<g
|
||||
id="g891"
|
||||
transform="matrix(0.186703,0,0,0.186703,-21.1073,57.62299)" />
|
||||
<g
|
||||
id="g3131"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="translate(-1,102)">
|
||||
<path
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
sodipodi:nodetypes="czcccccccscc"
|
||||
id="path3108"
|
||||
d="M 8.5,-94.500002 C 7.5975,-94.500002 7.168127,-94.186392 7,-93.04412 C 5.118026,-83.070943 5.215756,-74.96574 5.5,-64.864899 C 5.5,-64.115602 6.116307,-63.50001 6.875,-63.50001 L 42.125,-63.50001 C 42.88369,-63.50001 43.44816,-64.117293 43.5,-64.864899 C 44.5,-82.91177 42.5,-88.135111 42.5,-88.135111 C 42.5,-88.884408 41.88369,-89.5 41.125,-89.5 L 23.5,-89.5 L 22.5,-92.897058 C 22.254867,-93.729789 21.9025,-94.500002 21,-94.500002 L 8.5,-94.500002 z "
|
||||
style="fill:url(#linearGradient3938);fill-opacity:1;stroke:#555753;stroke-width:0.99999934;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3110"
|
||||
d="M 8.5,-93.5 C 8.167757,-93.5 8.1531396,-93.465856 8.15625,-93.46875 C 8.1593604,-93.471644 8.0371607,-93.339789 7.96875,-92.875 C 7.9689149,-92.864584 7.9689149,-92.854166 7.96875,-92.84375 C 6.2697576,-83.840251 6.2178889,-74.709407 6.4375,-65.5 L 42.625,-65.5 C 43.308005,-81.554915 41.5625,-87.875 41.5625,-87.875 C 41.530831,-87.955244 41.509819,-88.039293 41.5,-88.125 C 41.5,-88.333118 41.341065,-88.5 41.125,-88.5 L 23.5,-88.5 C 23.062797,-88.50549 22.681309,-88.797964 22.5625,-89.21875 L 21.5625,-92.625 C 21.45311,-92.996605 21.310238,-93.289949 21.21875,-93.40625 C 21.127262,-93.522551 21.173845,-93.5 21,-93.5 L 8.5,-93.5 z "
|
||||
style="opacity:0.5;fill:none;fill-opacity:1;stroke:url(#linearGradient3129);stroke-width:0.99999934;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="matrix(0.216083,0,0,0.263095,-1.89323,-11.2424)"
|
||||
id="g3112"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<path
|
||||
d="M 32.706693,164.36026 C 22.319193,164.36026 13.956693,172.72276 13.956693,183.11026 C 13.956693,193.49776 22.319193,201.86026 32.706693,201.86026 L 205.20669,201.86026 C 215.59419,201.86026 223.95669,193.49776 223.95669,183.11026 C 223.95669,172.72276 215.59419,164.36026 205.20669,164.36026 L 32.706693,164.36026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3114" />
|
||||
<path
|
||||
d="M 32.706693,165.61026 C 23.011693,165.61026 15.206693,173.41526 15.206693,183.11026 C 15.206693,192.80526 23.011693,200.61026 32.706693,200.61026 L 205.20669,200.61026 C 214.90169,200.61026 222.70669,192.80526 222.70669,183.11026 C 222.70669,173.41526 214.90169,165.61026 205.20669,165.61026 L 32.706693,165.61026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3116" />
|
||||
<path
|
||||
d="M 32.706694,166.86026 C 23.704194,166.86026 16.456694,174.10776 16.456694,183.11026 C 16.456694,192.11276 23.704194,199.36026 32.706694,199.36026 L 205.20669,199.36026 C 214.20919,199.36026 221.45669,192.11276 221.45669,183.11026 C 221.45669,174.10776 214.20919,166.86026 205.20669,166.86026 L 32.706694,166.86026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3118" />
|
||||
<path
|
||||
d="M 32.706694,168.11026 C 24.396694,168.11026 17.706694,174.80026 17.706694,183.11026 C 17.706694,191.42026 24.396694,198.11026 32.706694,198.11026 L 205.20669,198.11026 C 213.51669,198.11026 220.20669,191.42026 220.20669,183.11026 C 220.20669,174.80026 213.51669,168.11026 205.20669,168.11026 L 32.706694,168.11026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3120" />
|
||||
<path
|
||||
d="M 32.707764,169.36026 C 25.090264,169.36026 18.957764,175.49276 18.957764,183.11026 C 18.957764,190.72776 25.090264,196.86026 32.707764,196.86026 L 205.20618,196.86026 C 212.82368,196.86026 218.95618,190.72776 218.95618,183.11026 C 218.95618,175.49276 212.82368,169.36026 205.20618,169.36026 L 32.707764,169.36026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3122" />
|
||||
<path
|
||||
d="M 32.706694,170.61026 C 25.781694,170.61026 20.206694,176.18526 20.206694,183.11026 C 20.206694,190.03526 25.781694,195.61026 32.706694,195.61026 L 205.20669,195.61026 C 212.13169,195.61026 217.70669,190.03526 217.70669,183.11026 C 217.70669,176.18526 212.13169,170.61026 205.20669,170.61026 L 32.706694,170.61026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3124" />
|
||||
<path
|
||||
d="M 32.706694,171.86026 C 26.474194,171.86026 21.456694,176.87776 21.456694,183.11026 C 21.456694,189.34276 26.474194,194.36026 32.706694,194.36026 L 205.20669,194.36026 C 211.43919,194.36026 216.45669,189.34276 216.45669,183.11026 C 216.45669,176.87776 211.43919,171.86026 205.20669,171.86026 L 32.706694,171.86026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3126" />
|
||||
<path
|
||||
d="M 32.706694,173.11026 C 27.166694,173.11026 22.706694,177.57026 22.706694,183.11026 C 22.706694,188.65026 27.166694,193.11026 32.706694,193.11026 L 205.20669,193.11026 C 210.74669,193.11026 215.20669,188.65026 215.20669,183.11026 C 215.20669,177.57026 210.74669,173.11026 205.20669,173.11026 L 32.706694,173.11026 z "
|
||||
style="opacity:0.04787233;fill-rule:evenodd;stroke-width:3pt"
|
||||
id="path3128" />
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
id="g3661"
|
||||
transform="translate(-1,-2)"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<g
|
||||
id="g3663">
|
||||
<path
|
||||
sodipodi:type="inkscape:offset"
|
||||
inkscape:radius="-0.99436891"
|
||||
inkscape:original="M 2.65625 -33.5 C 2.021877 -33.5 1.5 -33.140103 1.5 -32.6875 C 4.500001 -25.004483 4.5 -23.258435 4.5 -16.5 C 4.5 -15.5 5.5111718 -13.51291 6.65625 -13.4375 L 42.34375 -13.4375 C 43.989096 -13.51291 44.500268 -15.758435 44.5 -16.5 C 44.5 -21.258435 44.500267 -25.004482 47.5 -32.6875 C 47.5 -33.140104 46.978125 -33.5 46.34375 -33.5 L 2.65625 -33.5 z "
|
||||
style="opacity:0.32156863;fill:none;fill-opacity:1;stroke:url(#linearGradient3685);stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline"
|
||||
id="path3667"
|
||||
d="M 2.625,-32.5 C 5.3597846,-25.370787 5.5,-23.037216 5.5,-16.5 C 5.5,-16.335159 5.6904313,-15.649773 6,-15.15625 C 6.3095687,-14.662727 6.6985247,-14.438832 6.71875,-14.4375 L 42.3125,-14.4375 L 42.34375,-14.4375 C 42.706073,-14.463351 42.916739,-14.679637 43.15625,-15.15625 C 43.401014,-15.643317 43.500051,-16.358058 43.5,-16.5 C 43.5,-21.130327 43.573003,-25.119501 46.375,-32.5 C 46.360642,-32.501163 46.358943,-32.5 46.34375,-32.5 L 2.65625,-32.5 C 2.6410568,-32.5 2.6393578,-32.501163 2.625,-32.5 z "
|
||||
transform="translate(0,54)" />
|
||||
<path
|
||||
style="opacity:0.3254902;fill:none;fill-rule:evenodd;stroke:white;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 2.5,21.5 L 46.5,21.5"
|
||||
id="path3669"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccc"
|
||||
id="path3671"
|
||||
d="M 2.645078,20.5 L 46.354654,20.5 C 46.989027,20.5 47.499732,20.862969 47.499732,21.313831 C 44.5,28.967284 44.5,32.694452 44.5,37.434576 C 44.500268,38.173288 44,40.423032 42.354654,40.498152 L 6.645078,40.498152 C 5.5,40.423032 4.5,38.430728 4.5,37.434576 C 4.5,30.702148 4.5,28.967284 1.499999,21.313831 C 1.499999,20.862969 2.010705,20.5 2.645078,20.5 z "
|
||||
style="fill:url(#linearGradient4238);fill-opacity:1;stroke:url(#linearGradient3687);stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3673"
|
||||
d="M 2.65625,21 C 2.4314409,21 2.2397168,21.044736 2.125,21.125 C 2.0102832,21.205264 2,21.274638 2,21.3125 C 4.2218558,27.007434 4.7883072,29.513938 4.9375,33.3125 L 44.125,33.3125 C 44.328358,30.089308 44.943507,26.607344 47,21.3125 C 47,21.274637 46.989716,21.205264 46.875,21.125 C 46.760284,21.044736 46.56856,21 46.34375,21 L 2.65625,21 z "
|
||||
style="opacity:0.8;fill:url(#linearGradient3689);fill-opacity:1;stroke:none;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
id="path3675"
|
||||
d="M 4.625,30 C 4.9603306,32.150219 5,34.257337 5,37.4375 C 5,37.771087 5.2086862,38.497978 5.5625,39.0625 C 5.9077942,39.613428 6.3538112,39.964089 6.65625,40 L 6.6875,40 L 42.3125,40 L 42.34375,40 C 42.929781,39.95968 43.308738,39.567494 43.59375,39 C 43.883899,38.422277 44.000093,37.694547 44,37.4375 C 44,34.957143 44.019836,32.683092 44.46875,30 L 4.625,30 z "
|
||||
style="opacity:0.44313725;fill:url(#linearGradient3691);fill-opacity:1;stroke:none;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline" />
|
||||
<path
|
||||
transform="translate(0,54)"
|
||||
d="M 2.625,-32.5 C 5.3597846,-25.370787 5.5,-23.037216 5.5,-16.5 C 5.5,-16.335159 5.6904313,-15.649773 6,-15.15625 C 6.3095687,-14.662727 6.6985247,-14.438832 6.71875,-14.4375 L 42.3125,-14.4375 L 42.34375,-14.4375 C 42.706073,-14.463351 42.916739,-14.679637 43.15625,-15.15625 C 43.401014,-15.643317 43.500051,-16.358058 43.5,-16.5 C 43.5,-21.130327 43.573003,-25.119501 46.375,-32.5 C 46.360642,-32.501163 46.358943,-32.5 46.34375,-32.5 L 2.65625,-32.5 C 2.6410568,-32.5 2.6393578,-32.501163 2.625,-32.5 z "
|
||||
id="path3677"
|
||||
style="opacity:0.2;fill:none;fill-opacity:1;stroke:url(#linearGradient3693);stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;display:inline"
|
||||
inkscape:original="M 2.65625 -33.5 C 2.021877 -33.5 1.5 -33.140103 1.5 -32.6875 C 4.500001 -25.004483 4.5 -23.258435 4.5 -16.5 C 4.5 -15.5 5.5111718 -13.51291 6.65625 -13.4375 L 42.34375 -13.4375 C 43.989096 -13.51291 44.500268 -15.758435 44.5 -16.5 C 44.5 -21.258435 44.500267 -25.004482 47.5 -32.6875 C 47.5 -33.140104 46.978125 -33.5 46.34375 -33.5 L 2.65625 -33.5 z "
|
||||
inkscape:radius="-0.99436891"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
<path
|
||||
sodipodi:nodetypes="cc"
|
||||
id="path3679"
|
||||
d="M 2.5,21.5 L 46.5,21.5"
|
||||
style="opacity:0.4;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient3695);stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g6102"
|
||||
inkscape:label="pixmap"
|
||||
style="display:inline"
|
||||
transform="translate(188.7605,-103.2651)" />
|
||||
<g
|
||||
transform="matrix(0.186703,0,0,0.186703,167.6532,-45.64211)"
|
||||
id="g6106" />
|
||||
<g
|
||||
inkscape:label="pattern"
|
||||
id="g30621"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
transform="translate(190.4218,35.092)" />
|
||||
<g
|
||||
inkscape:label="pattern"
|
||||
id="g35222"
|
||||
inkscape:r_cx="true"
|
||||
inkscape:r_cy="true"
|
||||
transform="translate(190.4218,81.092)" />
|
||||
<g
|
||||
id="g3198"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
transform="translate(-1,102)">
|
||||
<rect
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
ry="1.5"
|
||||
rx="1.5"
|
||||
y="-91.5"
|
||||
x="9.4999981"
|
||||
height="3"
|
||||
width="10.000002"
|
||||
id="rect2118"
|
||||
style="fill:#eeeeec;fill-opacity:1;stroke:#d3d7cf;stroke-width:0.99999988;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;display:inline" />
|
||||
<g
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/lapo/Icone/Crux/folderx-alt.png"
|
||||
transform="translate(66,0)"
|
||||
id="g3371"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1"
|
||||
id="rect3373"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-55"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3375"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-53"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3377"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-51"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
<rect
|
||||
style="opacity:1;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4.4000001;stroke-dasharray:none;stroke-dashoffset:10;stroke-opacity:1;display:inline"
|
||||
id="rect3379"
|
||||
width="1"
|
||||
height="1"
|
||||
x="-49"
|
||||
y="-90"
|
||||
rx="0.5"
|
||||
ry="0.5" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 24 KiB |
BIN
resources/images/devices/ipad.png
Normal file
After Width: | Height: | Size: 17 KiB |
2679
resources/images/drawer.svg
Normal file
After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 330 B After Width: | Height: | Size: 820 B |
BIN
resources/images/news/sarajevo_x.png
Normal file
After Width: | Height: | Size: 542 B |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
@ -5,7 +5,6 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
clarin.com
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Clarin(BasicNewsRecipe):
|
||||
@ -18,11 +17,12 @@ class Clarin(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
cover_url = strftime('http://www.clarin.com/diario/%Y/%m/%d/portada.jpg')
|
||||
encoding = 'cp1252'
|
||||
language = 'es'
|
||||
masthead_url = 'http://www.clarin.com/shared/v10/img/Hd/lg_Clarin.gif'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,"Times New Roman",Times,serif; font-size: xx-large} .Volan,.Pie,.Autor{ font-size: x-small} .Copete,.Hora{font-size: large} '
|
||||
encoding = 'utf8'
|
||||
language = 'es_AR'
|
||||
publication_type = 'newspaper'
|
||||
INDEX = 'http://www.clarin.com'
|
||||
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} '
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -31,27 +31,32 @@ class Clarin(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(name='a' , attrs={'class':'Imp' })
|
||||
,dict(name='div' , attrs={'class':'Perma' })
|
||||
,dict(name='h1' , text='Imprimir' )
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
|
||||
|
||||
feeds = [
|
||||
(u'Ultimo Momento', u'http://www.clarin.com/diario/hoy/um/sumariorss.xml')
|
||||
,(u'El Pais' , u'http://www.clarin.com/diario/hoy/elpais.xml' )
|
||||
,(u'Opinion' , u'http://www.clarin.com/diario/hoy/opinion.xml' )
|
||||
,(u'El Mundo' , u'http://www.clarin.com/diario/hoy/elmundo.xml' )
|
||||
,(u'Sociedad' , u'http://www.clarin.com/diario/hoy/sociedad.xml' )
|
||||
,(u'La Ciudad' , u'http://www.clarin.com/diario/hoy/laciudad.xml' )
|
||||
,(u'Policiales' , u'http://www.clarin.com/diario/hoy/policiales.xml' )
|
||||
,(u'Deportes' , u'http://www.clarin.com/diario/hoy/deportes.xml' )
|
||||
(u'Pagina principal', u'http://www.clarin.com/rss/' )
|
||||
,(u'Politica' , u'http://www.clarin.com/rss/politica/' )
|
||||
,(u'Deportes' , u'http://www.clarin.com/rss/deportes/' )
|
||||
,(u'Economia' , u'http://www.clarin.com/economia/' )
|
||||
,(u'Mundo' , u'http://www.clarin.com/rss/mundo/' )
|
||||
,(u'Espectaculos' , u'http://www.clarin.com/rss/espectaculos/')
|
||||
,(u'Sociedad' , u'http://www.clarin.com/rss/sociedad/' )
|
||||
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||
,(u'Policiales' , u'http://www.clarin.com/rss/policiales/' )
|
||||
,(u'Internet' , u'http://www.clarin.com/rss/internet/' )
|
||||
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
rest = url.partition('-0')[-1]
|
||||
lmain = rest.partition('.')[0]
|
||||
lurl = u'http://www.servicios.clarin.com/notas/jsp/clarin/v9/notas/imprimir.jsp?pagid=' + lmain
|
||||
return lurl
|
||||
return url + '?print=1'
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = None
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
cover_item = soup.find('div',attrs={'class':'bb-md bb-md-edicion_papel'})
|
||||
if cover_item:
|
||||
ap = cover_item.find('a',attrs={'href':'/edicion-impresa/'})
|
||||
if ap:
|
||||
cover_url = self.INDEX + ap.img['src']
|
||||
return cover_url
|
||||
|
||||
|
@ -1,189 +1,76 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import re
|
||||
from calibre import strftime
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
import string
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Newsweek(BasicNewsRecipe):
|
||||
|
||||
|
||||
title = 'Newsweek'
|
||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
||||
__author__ = 'Kovid Goyal'
|
||||
description = 'Weekly news and current affairs in the US'
|
||||
language = 'en'
|
||||
encoding = 'utf-8'
|
||||
no_stylesheets = True
|
||||
|
||||
extra_css = '''
|
||||
h1{font-family:Arial,Helvetica,sans-serif; font-size:large; color:#383733;}
|
||||
.deck{font-family:Georgia,sans-serif; color:#383733;}
|
||||
.bylineDate{font-family:georgia ; color:#58544A; font-size:x-small;}
|
||||
.authorInfo{font-family:arial,helvetica,sans-serif; color:#0066CC; font-size:x-small;}
|
||||
.articleUpdated{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
||||
.issueDate{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small; font-style:italic;}
|
||||
h5{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
||||
h6{font-family:arial,helvetica,sans-serif; color:#73726C; font-size:x-small;}
|
||||
.story{font-family:georgia,sans-serif ;color:black;}
|
||||
.photoCredit{color:#999999; font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
||||
.photoCaption{color:#0A0A09;font-family:Arial,Helvetica,sans-serif;font-size:x-small;}
|
||||
.fwArticle{font-family:Arial,Helvetica,sans-serif;font-size:x-small;font-weight:bold;}
|
||||
'''
|
||||
BASE_URL = 'http://www.newsweek.com'
|
||||
INDEX = BASE_URL+'/topics.html'
|
||||
|
||||
encoding = 'utf-8'
|
||||
language = 'en'
|
||||
keep_only_tags = dict(name='article', attrs={'class':'article-text'})
|
||||
remove_tags = [dict(attrs={'data-dartad':True})]
|
||||
remove_attributes = ['property']
|
||||
|
||||
remove_tags = [
|
||||
{'class':['fwArticle noHr','fwArticle','hdlBulletItem','head-content','navbar','link', 'ad', 'sponsorLinksArticle', 'mm-content',
|
||||
'inline-social-links-wrapper', 'email-article','ToolBox',
|
||||
'inline-promo-link', 'sponsorship',
|
||||
'inlineComponentRight',
|
||||
'comments-and-social-links-wrapper', 'EmailArticleBlock']},
|
||||
{'id' : ['footer', 'ticker-data', 'topTenVertical',
|
||||
'digg-top-five', 'mesothorax', 'nw-comments', 'my-take-landing',
|
||||
'ToolBox', 'EmailMain']},
|
||||
{'class': re.compile('related-cloud')},
|
||||
dict(name='li', attrs={'id':['slug_bigbox']})
|
||||
]
|
||||
def postprocess_html(self, soup, first):
|
||||
for tag in soup.findAll(name=['article', 'header']):
|
||||
tag.name = 'div'
|
||||
return soup
|
||||
|
||||
def newsweek_sections(self):
|
||||
soup = self.index_to_soup(self.INDEX)
|
||||
for a in soup.findAll('a', title='Primary tag', href=True):
|
||||
yield (string.capitalize(self.tag_to_string(a)),
|
||||
self.BASE_URL+a['href'])
|
||||
|
||||
|
||||
keep_only_tags = [{'class':['article HorizontalHeader',
|
||||
'articlecontent','photoBox', 'article columnist first']}, ]
|
||||
recursions = 1
|
||||
match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+']
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
|
||||
def find_title(self, section):
|
||||
d = {'scope':'Scope', 'thetake':'The Take', 'features':'Features',
|
||||
None:'Departments', 'culture':'Culture'}
|
||||
ans = None
|
||||
a = section.find('a', attrs={'name':True})
|
||||
if a is not None:
|
||||
ans = a['name']
|
||||
return d.get(ans, ans)
|
||||
|
||||
|
||||
def find_articles(self, section):
|
||||
ans = []
|
||||
for x in section.findAll('h5'):
|
||||
title = ' '.join(x.findAll(text=True)).strip()
|
||||
a = x.find('a')
|
||||
if not a: continue
|
||||
href = a['href']
|
||||
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
|
||||
if not ans:
|
||||
for x in section.findAll('div', attrs={'class':'hdlItem'}):
|
||||
a = x.find('a', href=True)
|
||||
if not a : continue
|
||||
title = ' '.join(a.findAll(text=True)).strip()
|
||||
href = a['href']
|
||||
if 'http://xtra.newsweek.com' in href: continue
|
||||
ans.append({'title':title, 'url':href, 'description':'', 'date': strftime('%a, %d %b')})
|
||||
|
||||
#for x in ans:
|
||||
# x['url'] += '/output/print'
|
||||
return ans
|
||||
def newsweek_parse_section_page(self, soup):
|
||||
for article in soup.findAll('article', about=True,
|
||||
attrs={'class':'stream-item'}):
|
||||
title = article.find(attrs={'property': 'dc:title'})
|
||||
if title is None: continue
|
||||
title = self.tag_to_string(title)
|
||||
url = self.BASE_URL + article['about']
|
||||
desc = ''
|
||||
author = article.find({'property':'dc:creator'})
|
||||
if author:
|
||||
desc = u'by %s. '%self.tag_to_string(author)
|
||||
p = article.find(attrs={'property':'dc:abstract'})
|
||||
if p is not None:
|
||||
for a in p.find('a'): a.extract()
|
||||
desc += self.tag_to_string(p)
|
||||
t = article.find('time', attrs={'property':'dc:created'})
|
||||
date = ''
|
||||
if t is not None:
|
||||
date = u' [%s]'%self.tag_to_string(t)
|
||||
self.log('\tFound article:', title, 'at', url)
|
||||
self.log('\t\t', desc)
|
||||
yield {'title':title, 'url':url, 'description':desc, 'date':date}
|
||||
|
||||
|
||||
def parse_index(self):
|
||||
soup = self.get_current_issue()
|
||||
if not soup:
|
||||
raise RuntimeError('Unable to connect to newsweek.com. Try again later.')
|
||||
sections = soup.findAll('div', attrs={'class':'featurewell'})
|
||||
titles = map(self.find_title, sections)
|
||||
articles = map(self.find_articles, sections)
|
||||
ans = list(zip(titles, articles))
|
||||
def fcmp(x, y):
|
||||
tx, ty = x[0], y[0]
|
||||
if tx == "Features": return cmp(1, 2)
|
||||
if ty == "Features": return cmp(2, 1)
|
||||
return cmp(tx, ty)
|
||||
return sorted(ans, cmp=fcmp)
|
||||
|
||||
def ensure_html(self, soup):
|
||||
root = soup.find(name=True)
|
||||
if root.name == 'html': return soup
|
||||
nsoup = BeautifulSoup('<html><head></head><body/></html>')
|
||||
nroot = nsoup.find(name='body')
|
||||
for x in soup.contents:
|
||||
if getattr(x, 'name', False):
|
||||
x.extract()
|
||||
nroot.insert(len(nroot), x)
|
||||
return nsoup
|
||||
|
||||
def postprocess_html(self, soup, first_fetch):
|
||||
if not first_fetch:
|
||||
h1 = soup.find(id='headline')
|
||||
if h1:
|
||||
h1.extract()
|
||||
div = soup.find(attrs={'class':'articleInfo'})
|
||||
if div:
|
||||
div.extract()
|
||||
divs = list(soup.findAll('div', 'pagination'))
|
||||
if not divs:
|
||||
return self.ensure_html(soup)
|
||||
for div in divs[1:]: div.extract()
|
||||
all_a = divs[0].findAll('a', href=True)
|
||||
divs[0]['style']="display:none"
|
||||
if len(all_a) > 1:
|
||||
all_a[-1].extract()
|
||||
test = re.compile(self.match_regexps[0])
|
||||
for a in soup.findAll('a', href=test):
|
||||
if a not in all_a:
|
||||
del a['href']
|
||||
return self.ensure_html(soup)
|
||||
|
||||
def get_current_issue(self):
|
||||
soup = self.index_to_soup('http://www.newsweek.com')
|
||||
div = soup.find('div', attrs={'class':re.compile('more-from-mag')})
|
||||
if div is None: return None
|
||||
a = div.find('a')
|
||||
if a is not None:
|
||||
href = a['href'].split('#')[0]
|
||||
return self.index_to_soup(href)
|
||||
|
||||
def get_cover_url(self):
|
||||
cover_url = None
|
||||
soup = self.index_to_soup('http://www.newsweek.com')
|
||||
link_item = soup.find('div',attrs={'class':'cover-image'})
|
||||
if link_item and link_item.a and link_item.a.img:
|
||||
cover_url = link_item.a.img['src']
|
||||
return cover_url
|
||||
sections = []
|
||||
for section, shref in self.newsweek_sections():
|
||||
self.log('Processing section', section, shref)
|
||||
articles = []
|
||||
soups = [self.index_to_soup(shref)]
|
||||
na = soups[0].find('a', rel='next')
|
||||
if na:
|
||||
soups.append(self.index_to_soup(self.BASE_URL+na['href']))
|
||||
for soup in soups:
|
||||
articles.extend(self.newsweek_parse_section_page(soup))
|
||||
if self.test and len(articles) > 1:
|
||||
break
|
||||
if articles:
|
||||
sections.append((section, articles))
|
||||
if self.test and len(sections) > 1:
|
||||
break
|
||||
return sections
|
||||
|
||||
|
||||
def postprocess_book(self, oeb, opts, log) :
|
||||
|
||||
def extractByline(href) :
|
||||
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
|
||||
byline = soup.find(True,attrs={'class':'authorInfo'})
|
||||
byline = self.tag_to_string(byline) if byline is not None else ''
|
||||
issueDate = soup.find(True,attrs={'class':'issueDate'})
|
||||
issueDate = self.tag_to_string(issueDate) if issueDate is not None else ''
|
||||
issueDate = re.sub(',','', issueDate)
|
||||
if byline > '' and issueDate > '' :
|
||||
return byline + ' | ' + issueDate
|
||||
else :
|
||||
return byline + issueDate
|
||||
|
||||
def extractDescription(href) :
|
||||
soup = BeautifulSoup(str(oeb.manifest.hrefs[href]))
|
||||
description = soup.find(True,attrs={'name':'description'})
|
||||
if description is not None and description.has_key('content'):
|
||||
description = description['content']
|
||||
if description.startswith('Newsweek magazine online plus') :
|
||||
description = soup.find(True, attrs={'class':'story'})
|
||||
firstPara = soup.find('p')
|
||||
description = self.tag_to_string(firstPara)
|
||||
else :
|
||||
description = soup.find(True, attrs={'class':'story'})
|
||||
firstPara = soup.find('p')
|
||||
description = self.tag_to_string(firstPara)
|
||||
return description
|
||||
|
||||
for section in oeb.toc :
|
||||
for article in section :
|
||||
if article.author is None :
|
||||
article.author = extractByline(article.href)
|
||||
if article.description is None :
|
||||
article.description = extractDescription(article.href)
|
||||
return
|
||||
|
||||
|
@ -391,10 +391,14 @@ class NYTimes(BasicNewsRecipe):
|
||||
return ans
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
# Skip ad pages before actual article
|
||||
# Skip ad pages served before actual article
|
||||
skip_tag = soup.find(True, {'name':'skip'})
|
||||
if skip_tag is not None:
|
||||
soup = self.index_to_soup(skip_tag.parent['href'])
|
||||
self.log.error("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.error("Skipping ad to article at '%s'" % url)
|
||||
soup = self.index_to_soup(url)
|
||||
return self.strip_anchors(soup)
|
||||
|
||||
def postprocess_html(self,soup, True):
|
||||
|
@ -280,18 +280,14 @@ class NYTimes(BasicNewsRecipe):
|
||||
return ans
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
'''
|
||||
refresh = soup.find('meta', {'http-equiv':'refresh'})
|
||||
if refresh is None:
|
||||
return soup
|
||||
content = refresh.get('content').partition('=')[2]
|
||||
raw = self.browser.open('http://www.nytimes.com'+content).read()
|
||||
return BeautifulSoup(raw.decode('cp1252', 'replace'))
|
||||
'''
|
||||
# Skip ad pages before actual article
|
||||
# Skip ad pages served before actual article
|
||||
skip_tag = soup.find(True, {'name':'skip'})
|
||||
if skip_tag is not None:
|
||||
soup = self.index_to_soup(skip_tag.parent['href'])
|
||||
self.log.error("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.error("Skipping ad to article at '%s'" % url)
|
||||
soup = self.index_to_soup(url)
|
||||
return self.strip_anchors(soup)
|
||||
|
||||
def postprocess_html(self,soup, True):
|
||||
|
66
resources/recipes/sarajevo_x.recipe
Normal file
@ -0,0 +1,66 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
|
||||
'''
|
||||
sarajevo-x.com
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
|
||||
|
||||
class SarajevoX(BasicNewsRecipe):
|
||||
title = 'Sarajevo-x.com'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Sarajevo-x.com - najposjeceniji bosanskohercegovacki internet portal'
|
||||
publisher = 'InterSoft d.o.o.'
|
||||
category = 'news, politics, Bosnia and Herzegovina,Sarajevo-x.com, internet, portal, vijesti, bosna i hercegovina, sarajevo'
|
||||
oldest_article = 2
|
||||
delay = 1
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'cp1250'
|
||||
use_embedded_content = False
|
||||
language = 'bs'
|
||||
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif} .article_description{font-family: sans1, sans-serif} div#fotka{display: block} img{margin-bottom: 0.5em} '
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'content-bg'})]
|
||||
remove_tags_after = dict(name='div',attrs={'class':'izvor'})
|
||||
remove_tags = [dict(name=['object','link','base','table'])]
|
||||
remove_attributes = ['height','width','alt','border']
|
||||
|
||||
feeds = [
|
||||
(u'BIH' , u'http://www.sarajevo-x.com/rss/bih' )
|
||||
,(u'Svijet' , u'http://www.sarajevo-x.com/rss/svijet' )
|
||||
,(u'Biznis' , u'http://www.sarajevo-x.com/rss/biznis' )
|
||||
,(u'Sport' , u'http://www.sarajevo-x.com/rss/sport' )
|
||||
,(u'Showtime' , u'http://www.sarajevo-x.com/rss/showtime' )
|
||||
,(u'Scitech' , u'http://www.sarajevo-x.com/rss/scitech' )
|
||||
,(u'Lifestyle' , u'http://www.sarajevo-x.com/rss/lifestyle' )
|
||||
,(u'Kultura' , u'http://www.sarajevo-x.com/rss/kultura' )
|
||||
,(u'Zanimljivosti', u'http://www.sarajevo-x.com/rss/zanimljivosti')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
dtag = soup.find('div',attrs={'id':'fotka'})
|
||||
if dtag:
|
||||
sp = soup.find('div',attrs={'id':'opisslike'})
|
||||
img = soup.find('img')
|
||||
if sp:
|
||||
sp
|
||||
else:
|
||||
mtag = Tag(soup,'div',[("id","opisslike"),("class","opscitech")])
|
||||
mopis = NavigableString("Opis")
|
||||
mtag.insert(0,mopis)
|
||||
img.append(mtag)
|
||||
return soup
|
||||
|
@ -21,12 +21,16 @@ class weltDe(BasicNewsRecipe):
|
||||
no_stylesheets = True
|
||||
remove_stylesheets = True
|
||||
remove_javascript = True
|
||||
encoding = 'iso-8859-1'
|
||||
BasicNewsRecipe.summary_length = 200
|
||||
encoding = 'utf-8'
|
||||
html2epub_options = 'linearize_tables = True\nbase_font_size2=10'
|
||||
BasicNewsRecipe.summary_length = 100
|
||||
|
||||
|
||||
remove_tags = [dict(id='jumplinks'),
|
||||
dict(id='ad1'),
|
||||
dict(id='top'),
|
||||
dict(id='header'),
|
||||
dict(id='additionalNavWrapper'),
|
||||
dict(id='fullimage_index'),
|
||||
dict(id='additionalNav'),
|
||||
dict(id='printMenu'),
|
||||
@ -35,6 +39,8 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(id='servicesBox'),
|
||||
dict(id='servicesNav'),
|
||||
dict(id='ad2'),
|
||||
dict(id='banner_1'),
|
||||
dict(id='ssoInfoTop'),
|
||||
dict(id='brandingWrapper'),
|
||||
dict(id='links-intern'),
|
||||
dict(id='navigation'),
|
||||
@ -53,10 +59,22 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(id='xmsg_comment'),
|
||||
dict(id='additionalNavWrapper'),
|
||||
dict(id='imagebox'),
|
||||
dict(id='footerContainer'),
|
||||
#dict(id=''),
|
||||
dict(name='span'),
|
||||
dict(name='div', attrs={'class':'printURL'}),
|
||||
dict(name='ul', attrs={'class':'clear mainNavigation inline'}),
|
||||
dict(name='ul', attrs={'class':'inline'}),
|
||||
dict(name='ul', attrs={'class':'ubar'}),
|
||||
dict(name='hr', attrs={'class':'ubar'}),
|
||||
dict(name='li', attrs={'class':'counter'}),
|
||||
dict(name='li', attrs={'class':'browseBack'}),
|
||||
dict(name='li', attrs={'class':'browseNext'}),
|
||||
dict(name='li', attrs={'class':'selected'}),
|
||||
dict(name='div', attrs={'class':'floatLeft'}),
|
||||
dict(name='div', attrs={'class':'ad'}),
|
||||
dict(name='div', attrs={'class':'ftBarLeft'}),
|
||||
dict(name='div', attrs={'class':'clear additionalNav'}),
|
||||
dict(name='div', attrs={'class':'inlineBox inlineFurtherLinks'}),
|
||||
dict(name='div', attrs={'class':'inlineBox videoInlineBox'}),
|
||||
dict(name='div', attrs={'class':'inlineGallery'}),
|
||||
@ -65,6 +83,23 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(name='div', attrs={'class':'articleOptions clear'}),
|
||||
dict(name='div', attrs={'class':'noPrint galleryIndex'}),
|
||||
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
|
||||
dict(name='div', attrs={'class':'clear module writeComment bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module textGallery bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module socialMedia bgColor1'}),
|
||||
dict(name='div', attrs={'class':'clear module continuativeLinks'}),
|
||||
dict(name='div', attrs={'class':'moreArtH3'}),
|
||||
dict(name='div', attrs={'class':'jqmWindow'}),
|
||||
dict(name='div', attrs={'class':'clear gap4'}),
|
||||
dict(name='div', attrs={'class':'hidden'}),
|
||||
dict(name='div', attrs={'class':'advertising'}),
|
||||
dict(name='div', attrs={'class':'ad adMarginBottom'}),
|
||||
dict(name='div', attrs={'class':'ad'}),
|
||||
dict(name='div', attrs={'class':'topLine'}),
|
||||
dict(name='div', attrs={'class':'toplineH2'}),
|
||||
dict(name='div', attrs={'class':'headLineH3'}),
|
||||
dict(name='div', attrs={'class':'print'}),
|
||||
dict(name='div', attrs={'class':'clear menu'}),
|
||||
dict(name='div', attrs={'class':'clear galleryContent'}),
|
||||
dict(name='p', attrs={'class':'jump'}),
|
||||
dict(name='a', attrs={'class':'commentLink'}),
|
||||
dict(name='h2', attrs={'class':'jumpHeading'}),
|
||||
@ -75,7 +110,7 @@ class weltDe(BasicNewsRecipe):
|
||||
dict(name='table', attrs={'class':'textGallery'}),
|
||||
dict(name='li', attrs={'class':'active'})]
|
||||
|
||||
remove_tags_after = [dict(id='tw_link_widget')]
|
||||
remove_tags_after = [dict(name='div', attrs={'class':'clear departmentLine'})]
|
||||
|
||||
extra_css = '''
|
||||
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
|
||||
@ -87,7 +122,6 @@ class weltDe(BasicNewsRecipe):
|
||||
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
|
||||
|
||||
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
|
||||
('Deutsche Dinge', 'http://www.welt.de/deutsche-dinge/?service=Rss'),
|
||||
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
|
||||
('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
|
||||
('Sport', 'http://welt.de/sport/?service=Rss'),
|
||||
@ -101,4 +135,5 @@ class weltDe(BasicNewsRecipe):
|
||||
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace ('.html', '.html?print=yes')
|
||||
return url.replace ('.html', '.html?print=true')
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?python
|
||||
from uuid import uuid4
|
||||
import re
|
||||
?>
|
||||
<ncx version="2005-1"
|
||||
xml:lang="en"
|
||||
xmlns="http://www.daisy.org/z3986/2005/ncx/"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata"
|
||||
>
|
||||
<head>
|
||||
<meta name="dtb:uid" content="${uid}"/>
|
||||
<meta name="dtb:depth" content="${toc.depth()}"/>
|
||||
<meta name="dtb:generator" content="${__appname__}"/>
|
||||
<meta name="dtb:totalPageCount" content="0"/>
|
||||
<meta name="dtb:maxPageNumber" content="0"/>
|
||||
</head>
|
||||
<docTitle><text>Table of Contents</text></docTitle>
|
||||
|
||||
<py:def function="navpoint(np, level)">
|
||||
${'%*s'%(4*level,'')}<navPoint id="${str(uuid4())}" playOrder="${str(np.play_order)}">
|
||||
${'%*s'%(4*level,'')}<navLabel>
|
||||
${'%*s'%(4*level,'')}<text>${re.sub(r'\s+', ' ', np.text)}</text>
|
||||
${'%*s'%(4*level,'')}</navLabel>
|
||||
${'%*s'%(4*level,'')}<content src="${unicode(np.href)+(('#' + unicode(np.fragment)) if np.fragment else '')}" />
|
||||
${'%*s'%(4*level,'')}<calibre:meta py:if="np.author" name="author">${np.author}</calibre:meta>
|
||||
${'%*s'%(4*level,'')}<calibre:meta py:if="np.description" name="description">${np.description}</calibre:meta>
|
||||
<py:for each="np2 in np">${navpoint(np2, level+1)}</py:for>
|
||||
${'%*s'%(4*level,'')}</navPoint>
|
||||
</py:def>
|
||||
<navMap>
|
||||
<py:for each="np in toc">${navpoint(np, 0)}</py:for>
|
||||
</navMap>
|
||||
</ncx>
|
@ -1,49 +0,0 @@
|
||||
<package version="2.0"
|
||||
xmlns="http://www.idpf.org/2007/opf"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
unique-identifier="${__appname__}_id"
|
||||
|
||||
>
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf" xmlns:calibre="http://calibre.kovidgoyal.net/2009/metadata">
|
||||
<dc:title py:with="attrs={'opf:file-as':mi.title_sort}" py:attrs="attrs">${mi.title}</dc:title>
|
||||
<dc:creator opf:role="aut" py:for="i, author in enumerate(mi.authors)" py:attrs="{'opf:file-as':mi.author_sort} if mi.author_sort and i == 0 else {}">${author}</dc:creator>
|
||||
<dc:contributor opf:role="bkp" py:with="attrs={'opf:file-as':__appname__}" py:attrs="attrs">${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]</dc:contributor>
|
||||
<dc:identifier opf:scheme="${__appname__}" id="${__appname__}_id">${mi.application_id}</dc:identifier>
|
||||
<dc:date py:if="getattr(mi, 'pubdate', None) is not None">${mi.pubdate.isoformat()}</dc:date>
|
||||
<dc:language>${mi.language if mi.language else 'UND'}</dc:language>
|
||||
<dc:type py:if="getattr(mi, 'category', False)">${mi.category}</dc:type>
|
||||
<dc:description py:if="mi.comments">${mi.comments}</dc:description>
|
||||
<dc:publisher py:if="mi.publisher">${mi.publisher}</dc:publisher>
|
||||
<dc:identifier opf:scheme="ISBN" py:if="mi.isbn">${mi.isbn}</dc:identifier>
|
||||
<dc:rights py:if="mi.rights">${mi.rights}</dc:rights>
|
||||
<meta py:if="mi.series is not None" name="calibre:series" content="${mi.series}"/>
|
||||
<meta py:if="mi.series_index is not None" name="calibre:series_index" content="${mi.format_series_index()}"/>
|
||||
<meta py:if="mi.rating is not None" name="calibre:rating" content="${mi.rating}"/>
|
||||
<meta py:if="mi.timestamp is not None" name="calibre:timestamp" content="${mi.timestamp.isoformat()}"/>
|
||||
<meta py:if="mi.publication_type is not None" name="calibre:publication_type" content="${mi.publication_type}" />
|
||||
<py:for each="tag in mi.tags">
|
||||
<dc:subject py:if="mi.tags is not None">${tag}</dc:subject>
|
||||
</py:for>
|
||||
</metadata>
|
||||
|
||||
<manifest py:if="getattr(mi, 'manifest', None)">
|
||||
<py:for each="ref in mi.manifest">
|
||||
<item id="${ref.id}" href="${ref.href()}" media-type="${ref.mime_type}" />
|
||||
</py:for>
|
||||
</manifest>
|
||||
|
||||
<guide py:if="getattr(mi, 'guide', None)">
|
||||
<py:for each="ref in mi.guide">
|
||||
<reference type="${ref.type}" href="${ref.href()}" py:with="attrs={'title': ref.title if ref.title else None}" py:attrs="attrs" />
|
||||
</py:for>
|
||||
</guide>
|
||||
|
||||
<spine py:if="getattr(mi, 'spine', None)"
|
||||
py:with="attrs={'toc':'ncx' if mi.toc else None}" py:attrs="attrs">
|
||||
<py:for each="resource in mi.spine">
|
||||
<itemref idref="${resource.id}" />
|
||||
</py:for>
|
||||
</spine>
|
||||
|
||||
|
||||
</package>
|
@ -141,7 +141,10 @@ def prints(*args, **kwargs):
|
||||
raise
|
||||
arg = repr(arg)
|
||||
|
||||
try:
|
||||
file.write(arg)
|
||||
except:
|
||||
file.write(repr(arg))
|
||||
if i != len(args)-1:
|
||||
file.write(sep)
|
||||
file.write(end)
|
||||
@ -445,6 +448,23 @@ def prepare_string_for_xml(raw, attribute=False):
|
||||
raw = raw.replace('"', '"').replace("'", ''')
|
||||
return raw
|
||||
|
||||
def isbytestring(obj):
|
||||
return isinstance(obj, (str, bytes))
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
divisor, suffix = 1, "B"
|
||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
||||
if size < 1024**(i+1):
|
||||
divisor, suffix = 1024**(i), candidate
|
||||
break
|
||||
size = str(float(size)/divisor)
|
||||
if size.find(".") > -1:
|
||||
size = size[:size.find(".")+2]
|
||||
if size.endswith('.0'):
|
||||
size = size[:-2]
|
||||
return size + " " + suffix
|
||||
|
||||
if isosx:
|
||||
import glob, shutil
|
||||
fdir = os.path.expanduser('~/.fonts')
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.6.55'
|
||||
__version__ = '0.7.0'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -29,7 +29,7 @@ class Plugin(object):
|
||||
|
||||
'''
|
||||
#: List of platforms this plugin works on
|
||||
#: For example: ``['windows', 'osx', 'linux']
|
||||
#: For example: ``['windows', 'osx', 'linux']``
|
||||
supported_platforms = []
|
||||
|
||||
#: The name of this plugin. You must set it something other
|
||||
@ -214,10 +214,8 @@ class MetadataReaderPlugin(Plugin):
|
||||
Return metadata for the file represented by stream (a file like object
|
||||
that supports reading). Raise an exception when there is an error
|
||||
with the input data.
|
||||
|
||||
:param type: The type of file. Guaranteed to be one of the entries
|
||||
in :attr:`file_types`.
|
||||
|
||||
:return: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
||||
'''
|
||||
return None
|
||||
@ -245,11 +243,9 @@ class MetadataWriterPlugin(Plugin):
|
||||
Set metadata for the file represented by stream (a file like object
|
||||
that supports reading). Raise an exception when there is an error
|
||||
with the input data.
|
||||
|
||||
:param type: The type of file. Guaranteed to be one of the entries
|
||||
in :attr:`file_types`.
|
||||
:param mi: A :class:`calibre.ebooks.metadata.MetaInformation` object
|
||||
|
||||
'''
|
||||
pass
|
||||
|
||||
|
@ -430,7 +430,7 @@ from calibre.ebooks.txt.output import TXTOutput
|
||||
|
||||
from calibre.customize.profiles import input_profiles, output_profiles
|
||||
|
||||
|
||||
from calibre.devices.apple.driver import ITUNES
|
||||
from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX
|
||||
from calibre.devices.blackberry.driver import BLACKBERRY
|
||||
from calibre.devices.cybook.driver import CYBOOK
|
||||
@ -442,8 +442,7 @@ from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
||||
from calibre.devices.jetbook.driver import JETBOOK
|
||||
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
|
||||
from calibre.devices.nook.driver import NOOK
|
||||
from calibre.devices.prs500.driver import PRS500
|
||||
from calibre.devices.prs505.driver import PRS505, PRS700
|
||||
from calibre.devices.prs505.driver import PRS505
|
||||
from calibre.devices.android.driver import ANDROID, S60
|
||||
from calibre.devices.nokia.driver import N770, N810, E71X
|
||||
from calibre.devices.eslick.driver import ESLICK
|
||||
@ -455,6 +454,7 @@ from calibre.devices.edge.driver import EDGE
|
||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
|
||||
from calibre.devices.sne.driver import SNE
|
||||
from calibre.devices.misc import PALMPRE, AVANT
|
||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||
from calibre.devices.kobo.driver import KOBO
|
||||
|
||||
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
|
||||
@ -496,6 +496,7 @@ plugins += [
|
||||
]
|
||||
# Order here matters. The first matched device is the one used.
|
||||
plugins += [
|
||||
ITUNES,
|
||||
HANLINV3,
|
||||
HANLINV5,
|
||||
BLACKBERRY,
|
||||
@ -511,8 +512,6 @@ plugins += [
|
||||
KINDLE_DX,
|
||||
NOOK,
|
||||
PRS505,
|
||||
PRS700,
|
||||
PRS500,
|
||||
ANDROID,
|
||||
S60,
|
||||
N770,
|
||||
@ -544,6 +543,7 @@ plugins += [
|
||||
PALMPRE,
|
||||
KOBO,
|
||||
AZBOOKA,
|
||||
FOLDER_DEVICE_FOR_CONFIG,
|
||||
AVANT,
|
||||
]
|
||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||
|
@ -237,6 +237,12 @@ class OutputProfile(Plugin):
|
||||
# If True the MOBI renderer on the device supports MOBI indexing
|
||||
supports_mobi_indexing = False
|
||||
|
||||
# Device supports displaying a nested TOC
|
||||
supports_nested_toc = True
|
||||
|
||||
# If True output should be optimized for a touchscreen interface
|
||||
touchscreen = False
|
||||
|
||||
@classmethod
|
||||
def tags_to_string(cls, tags):
|
||||
return escape(', '.join(tags))
|
||||
@ -250,6 +256,8 @@ class iPadOutput(OutputProfile):
|
||||
screen_size = (768, 1024)
|
||||
comic_screen_size = (768, 1024)
|
||||
dpi = 132.0
|
||||
supports_nested_toc = False
|
||||
touchscreen = True
|
||||
|
||||
class SonyReaderOutput(OutputProfile):
|
||||
|
||||
|
@ -27,6 +27,34 @@ def strftime(epoch, zone=time.gmtime):
|
||||
src[2] = INVERSE_MONTH_MAP[int(src[2])]
|
||||
return ' '.join(src)
|
||||
|
||||
def get_connected_device():
|
||||
from calibre.customize.ui import device_plugins
|
||||
from calibre.devices.scanner import DeviceScanner
|
||||
dev = None
|
||||
scanner = DeviceScanner()
|
||||
scanner.scan()
|
||||
connected_devices = []
|
||||
for d in device_plugins():
|
||||
ok, det = scanner.is_device_connected(d)
|
||||
if ok:
|
||||
dev = d
|
||||
dev.reset(log_packets=False, detected_device=det)
|
||||
connected_devices.append(dev)
|
||||
|
||||
if dev is None:
|
||||
print >>sys.stderr, 'Unable to find a connected ebook reader.'
|
||||
return
|
||||
|
||||
for d in connected_devices:
|
||||
try:
|
||||
d.open()
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
dev = d
|
||||
break
|
||||
return dev
|
||||
|
||||
def debug(ioreg_to_tmp=False, buf=None):
|
||||
from calibre.customize.ui import device_plugins
|
||||
from calibre.devices.scanner import DeviceScanner, win_pnp_drives
|
||||
|
2
src/calibre/devices/apple/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
1524
src/calibre/devices/apple/driver.py
Normal file
@ -25,6 +25,17 @@ class DeviceError(ProtocolError):
|
||||
msg = "Unable to find SONY Reader. Is it connected?"
|
||||
ProtocolError.__init__(self, msg)
|
||||
|
||||
class UserFeedback(DeviceError):
|
||||
INFO = 0
|
||||
WARN = WARNING = 1
|
||||
ERROR = 2
|
||||
|
||||
def __init__(self, msg, details, level):
|
||||
Exception.__init__(self, msg)
|
||||
self.level = level
|
||||
self.details = details
|
||||
self.msg = msg
|
||||
|
||||
class DeviceBusy(ProtocolError):
|
||||
""" Raised when device is busy """
|
||||
def __init__(self, uerr=""):
|
||||
|
10
src/calibre/devices/folder_device/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
|
93
src/calibre/devices/folder_device/driver.py
Normal file
@ -0,0 +1,93 @@
|
||||
'''
|
||||
Created on 15 May 2010
|
||||
|
||||
@author: charles
|
||||
'''
|
||||
import os
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS, BookList
|
||||
|
||||
# This class is added to the standard device plugin chain, so that it can
|
||||
# be configured. It has invalid vendor_id etc, so it will never match a
|
||||
# device. The 'real' FOLDER_DEVICE will use the config from it.
|
||||
class FOLDER_DEVICE_FOR_CONFIG(USBMS):
|
||||
name = 'Folder Device Interface'
|
||||
gui_name = 'Folder Device'
|
||||
description = _('Use an arbitrary folder as a device.')
|
||||
author = 'John Schember/Charles Haley'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
|
||||
VENDOR_ID = 0xffff
|
||||
PRODUCT_ID = 0xffff
|
||||
BCD = 0xffff
|
||||
|
||||
|
||||
class FOLDER_DEVICE(USBMS):
|
||||
type = _('Device Interface')
|
||||
|
||||
name = 'Folder Device Interface'
|
||||
gui_name = 'Folder Device'
|
||||
description = _('Use an arbitrary folder as a device.')
|
||||
author = 'John Schember/Charles Haley'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
FORMATS = ['epub', 'fb2', 'mobi', 'lrf', 'tcr', 'pmlz', 'lit', 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb']
|
||||
|
||||
VENDOR_ID = 0xffff
|
||||
PRODUCT_ID = 0xffff
|
||||
BCD = 0xffff
|
||||
|
||||
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
|
||||
|
||||
CAN_SET_METADATA = True
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
#: Icon for this device
|
||||
icon = I('devices/folder.svg')
|
||||
METADATA_CACHE = '.metadata.calibre'
|
||||
|
||||
_main_prefix = ''
|
||||
_card_a_prefix = None
|
||||
_card_b_prefix = None
|
||||
|
||||
is_connected = False
|
||||
|
||||
def __init__(self, path):
|
||||
if not os.path.isdir(path):
|
||||
raise IOError, 'Path is not a folder'
|
||||
path = USBMS.normalize_path(path)
|
||||
if path.endswith(os.sep):
|
||||
self._main_prefix = path
|
||||
else:
|
||||
self._main_prefix = path + os.sep
|
||||
self.booklist_class = BookList
|
||||
self.is_connected = True
|
||||
|
||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||
detected_device=None):
|
||||
pass
|
||||
|
||||
def disconnect_from_folder(self):
|
||||
self._main_prefix = ''
|
||||
self.is_connected = False
|
||||
|
||||
def is_usb_connected(self, devices_on_system, debug=False,
|
||||
only_presence=False):
|
||||
return self.is_connected, self
|
||||
|
||||
def open(self):
|
||||
if not self._main_prefix:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_progress_reporter(self, report_progress):
|
||||
self.report_progress = report_progress
|
||||
|
||||
def card_prefix(self, end_session=True):
|
||||
return (None, None)
|
||||
|
||||
def eject(self):
|
||||
self.is_connected = False
|
||||
|
||||
@classmethod
|
||||
def settings(self):
|
||||
return FOLDER_DEVICE_FOR_CONFIG._config().parse()
|
@ -47,6 +47,10 @@ class DevicePlugin(Plugin):
|
||||
# Used by gui2.ui:annotations_fetched() and devices.kindle.driver:get_annotations()
|
||||
UserAnnotation = namedtuple('Annotation','type, value')
|
||||
|
||||
#: GUI displays this as a message if not None. Useful if opening can take a
|
||||
#: long time
|
||||
OPEN_FEEDBACK_MESSAGE = None
|
||||
|
||||
@classmethod
|
||||
def get_gui_name(cls):
|
||||
if hasattr(cls, 'gui_name'):
|
||||
@ -293,8 +297,7 @@ class DevicePlugin(Plugin):
|
||||
put the book. len(metadata) == len(files). Apart from the regular
|
||||
cover (path to cover), there may also be a thumbnail attribute, which should
|
||||
be used in preference. The thumbnail attribute is of the form
|
||||
(width, height, cover_data as jpeg). In addition the MetaInformation
|
||||
objects can have a tag_order attribute.
|
||||
(width, height, cover_data as jpeg).
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
@ -380,22 +383,46 @@ class BookList(list):
|
||||
3. size (file size of the book)
|
||||
4. datetime (a UTC time tuple)
|
||||
5. path (path on the device to the book)
|
||||
6. thumbnail (can be None)
|
||||
6. thumbnail (can be None) thumbnail is either a str/bytes object with the
|
||||
image data or it should have an attribute image_path that stores an
|
||||
absolute (platform native) path to the image
|
||||
7. tags (a list of strings, can be empty).
|
||||
'''
|
||||
|
||||
__getslice__ = None
|
||||
__setslice__ = None
|
||||
|
||||
def supports_tags(self):
|
||||
''' Return True if the the device supports tags (collections) for this book list. '''
|
||||
def __init__(self, oncard, prefix, settings):
|
||||
pass
|
||||
|
||||
def supports_collections(self):
|
||||
''' Return True if the the device supports collections for this book list. '''
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
def add_book(self, book, replace_metadata):
|
||||
'''
|
||||
Set the tags for C{book} to C{tags}.
|
||||
@param tags: A list of strings. Can be empty.
|
||||
@param book: A book object that is in this BookList.
|
||||
Add the book to the booklist. Intent is to maintain any device-internal
|
||||
metadata. Return True if booklists must be sync'ed
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_book(self, book):
|
||||
'''
|
||||
Remove a book from the booklist. Correct any device metadata at the
|
||||
same time
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_collections(self, collection_attributes):
|
||||
'''
|
||||
Return a dictionary of collections created from collection_attributes.
|
||||
Each entry in the dictionary is of the form collection name:[list of
|
||||
books]
|
||||
|
||||
The list of books is sorted by book title, except for collections
|
||||
created from series, in which case series_index is used.
|
||||
|
||||
:param collection_attributes: A list of attributes of the Book object
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -60,11 +60,6 @@ class KINDLE(USBMS):
|
||||
'replace')
|
||||
return mi
|
||||
|
||||
def filename_callback(self, fname, mi):
|
||||
if fname.startswith('.'):
|
||||
return 'x'+fname[1:]
|
||||
return fname
|
||||
|
||||
def get_annotations(self, path_map):
|
||||
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
|
||||
mbp_formats = set(MBP_FORMATS)
|
||||
|
@ -1,2 +1,6 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
MEDIA_XML = 'database/cache/media.xml'
|
||||
|
||||
CACHE_XML = 'Sony Reader/database/cache.xml'
|
||||
|
@ -1,449 +0,0 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''
|
||||
'''
|
||||
import re, time, functools
|
||||
from uuid import uuid4 as _uuid
|
||||
import xml.dom.minidom as dom
|
||||
from base64 import b64decode as decode
|
||||
from base64 import b64encode as encode
|
||||
|
||||
|
||||
from calibre.devices.interface import BookList as _BookList
|
||||
from calibre.devices import strftime as _strftime
|
||||
from calibre.devices import strptime
|
||||
|
||||
strftime = functools.partial(_strftime, zone=time.gmtime)
|
||||
|
||||
MIME_MAP = {
|
||||
"lrf" : "application/x-sony-bbeb",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
"rtf" : "application/rtf",
|
||||
"pdf" : "application/pdf",
|
||||
"txt" : "text/plain" ,
|
||||
'epub': 'application/epub+zip',
|
||||
}
|
||||
|
||||
def uuid():
|
||||
return str(_uuid()).replace('-', '', 1).upper()
|
||||
|
||||
def sortable_title(title):
|
||||
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
|
||||
|
||||
class book_metadata_field(object):
|
||||
""" Represents metadata stored as an attribute """
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
self.formatter = formatter
|
||||
self.setter = setter
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
""" Return a string. String may be empty if self.attr is absent """
|
||||
return self.formatter(obj.elem.getAttribute(self.attr)) if \
|
||||
self.formatter else obj.elem.getAttribute(self.attr).strip()
|
||||
|
||||
def __set__(self, obj, val):
|
||||
""" Set the attribute """
|
||||
val = self.setter(val) if self.setter else val
|
||||
if not isinstance(val, unicode):
|
||||
val = unicode(val, 'utf8', 'replace')
|
||||
obj.elem.setAttribute(self.attr, val)
|
||||
|
||||
|
||||
class Book(object):
|
||||
""" Provides a view onto the XML element that represents a book """
|
||||
|
||||
title = book_metadata_field("title")
|
||||
authors = book_metadata_field("author", \
|
||||
formatter=lambda x: x if x and x.strip() else _('Unknown'))
|
||||
mime = book_metadata_field("mime")
|
||||
rpath = book_metadata_field("path")
|
||||
id = book_metadata_field("id", formatter=int)
|
||||
sourceid = book_metadata_field("sourceid", formatter=int)
|
||||
size = book_metadata_field("size", formatter=lambda x : int(float(x)))
|
||||
# When setting this attribute you must use an epoch
|
||||
datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
|
||||
|
||||
@dynamic_property
|
||||
def title_sorter(self):
|
||||
doc = '''String to sort the title. If absent, title is returned'''
|
||||
def fget(self):
|
||||
src = self.elem.getAttribute('titleSorter').strip()
|
||||
if not src:
|
||||
src = self.title
|
||||
return src
|
||||
def fset(self, val):
|
||||
self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
|
||||
return property(doc=doc, fget=fget, fset=fset)
|
||||
|
||||
@dynamic_property
|
||||
def thumbnail(self):
|
||||
doc = \
|
||||
"""
|
||||
The thumbnail. Should be a height 68 image.
|
||||
Setting is not supported.
|
||||
"""
|
||||
def fget(self):
|
||||
th = self.elem.getElementsByTagName(self.prefix + "thumbnail")
|
||||
if not len(th):
|
||||
th = self.elem.getElementsByTagName("cache:thumbnail")
|
||||
if len(th):
|
||||
for n in th[0].childNodes:
|
||||
if n.nodeType == n.ELEMENT_NODE:
|
||||
th = n
|
||||
break
|
||||
rc = ""
|
||||
for node in th.childNodes:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc += node.data
|
||||
return decode(rc)
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
@dynamic_property
|
||||
def path(self):
|
||||
doc = """ Absolute path to book on device. Setting not supported. """
|
||||
def fget(self):
|
||||
return self.mountpath + self.rpath
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
@dynamic_property
|
||||
def db_id(self):
|
||||
doc = '''The database id in the application database that this file corresponds to'''
|
||||
def fget(self):
|
||||
match = re.search(r'_(\d+)$', self.rpath.rpartition('.')[0])
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
def __init__(self, node, mountpath, tags, prefix=""):
|
||||
self.elem = node
|
||||
self.prefix = prefix
|
||||
self.tags = tags
|
||||
self.mountpath = mountpath
|
||||
|
||||
def __str__(self):
|
||||
""" Return a utf-8 encoded string with title author and path information """
|
||||
return self.title.encode('utf-8') + " by " + \
|
||||
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
|
||||
|
||||
|
||||
class BookList(_BookList):
|
||||
|
||||
def __init__(self, xml_file, mountpath, report_progress=None):
|
||||
_BookList.__init__(self)
|
||||
xml_file.seek(0)
|
||||
self.document = dom.parse(xml_file)
|
||||
self.root_element = self.document.documentElement
|
||||
self.mountpath = mountpath
|
||||
records = self.root_element.getElementsByTagName('records')
|
||||
self.tag_order = {}
|
||||
|
||||
if records:
|
||||
self.prefix = 'xs1:'
|
||||
self.root_element = records[0]
|
||||
else:
|
||||
self.prefix = ''
|
||||
|
||||
nodes = self.root_element.childNodes
|
||||
for i, book in enumerate(nodes):
|
||||
if report_progress:
|
||||
report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...'))
|
||||
if hasattr(book, 'tagName') and book.tagName.endswith('text'):
|
||||
tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))]
|
||||
self.append(Book(book, mountpath, tags, prefix=self.prefix))
|
||||
|
||||
def max_id(self):
|
||||
max = 0
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
nid = int(child.getAttribute('id'))
|
||||
if nid > max:
|
||||
max = nid
|
||||
return max
|
||||
|
||||
def is_id_valid(self, id):
|
||||
'''Return True iff there is an element with C{id==id}.'''
|
||||
id = str(id)
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("id"):
|
||||
if child.getAttribute('id') == id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def supports_tags(self):
|
||||
return True
|
||||
|
||||
def book_by_path(self, path):
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
|
||||
if path == child.getAttribute('path'):
|
||||
return child
|
||||
return None
|
||||
|
||||
def add_book(self, mi, name, collections, size, ctime):
|
||||
""" Add a node into the DOM tree, representing a book """
|
||||
book = self.book_by_path(name)
|
||||
if book is not None:
|
||||
self.remove_book(name)
|
||||
|
||||
node = self.document.createElement(self.prefix + "text")
|
||||
mime = MIME_MAP.get(name.rpartition('.')[-1].lower(), MIME_MAP['epub'])
|
||||
cid = self.max_id()+1
|
||||
try:
|
||||
sourceid = str(self[0].sourceid) if len(self) else '1'
|
||||
except:
|
||||
sourceid = '1'
|
||||
attrs = {
|
||||
"title" : mi.title,
|
||||
'titleSorter' : sortable_title(mi.title),
|
||||
"author" : mi.format_authors() if mi.format_authors() else _('Unknown'),
|
||||
"page":"0", "part":"0", "scale":"0", \
|
||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||
"mime":mime, "path":name, "size":str(size)
|
||||
}
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
try:
|
||||
w, h, data = mi.thumbnail
|
||||
except:
|
||||
w, h, data = None, None, None
|
||||
|
||||
if data:
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th.setAttribute("width", str(w))
|
||||
th.setAttribute("height", str(h))
|
||||
jpeg = self.document.createElement(self.prefix + "jpeg")
|
||||
jpeg.appendChild(self.document.createTextNode(encode(data)))
|
||||
th.appendChild(jpeg)
|
||||
node.appendChild(th)
|
||||
self.root_element.appendChild(node)
|
||||
book = Book(node, self.mountpath, [], prefix=self.prefix)
|
||||
book.datetime = ctime
|
||||
self.append(book)
|
||||
|
||||
tags = []
|
||||
for item in collections:
|
||||
item = item.strip()
|
||||
mitem = getattr(mi, item, None)
|
||||
titems = []
|
||||
if mitem:
|
||||
if isinstance(mitem, list):
|
||||
titems = mitem
|
||||
else:
|
||||
titems = [mitem]
|
||||
if item == 'tags' and titems:
|
||||
litems = []
|
||||
for i in titems:
|
||||
if not i.strip().startswith('[') and not i.strip().endswith(']'):
|
||||
litems.append(i)
|
||||
titems = litems
|
||||
tags.extend(titems)
|
||||
if tags:
|
||||
tags = list(set(tags))
|
||||
if hasattr(mi, 'tag_order'):
|
||||
self.tag_order.update(mi.tag_order)
|
||||
self.set_tags(book, tags)
|
||||
|
||||
def _delete_book(self, node):
|
||||
nid = node.getAttribute('id')
|
||||
self.remove_from_playlists(nid)
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
|
||||
def delete_book(self, cid):
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{id == cid}.
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for book in self:
|
||||
if str(book.id) == str(cid):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
def remove_book(self, path):
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{path == path}.
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for book in self:
|
||||
if path.endswith(book.rpath):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
def playlists(self):
|
||||
ans = []
|
||||
for c in self.root_element.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('playlist'):
|
||||
ans.append(c)
|
||||
return ans
|
||||
|
||||
def playlist_items(self):
|
||||
plitems = []
|
||||
for pl in self.playlists():
|
||||
for c in pl.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('item') and \
|
||||
hasattr(c, 'getAttribute'):
|
||||
try:
|
||||
c.getAttribute('id')
|
||||
except: # Unlinked node
|
||||
continue
|
||||
plitems.append(c)
|
||||
return plitems
|
||||
|
||||
def purge_corrupted_files(self):
|
||||
if not self.root_element:
|
||||
return []
|
||||
corrupted = self.root_element.getElementsByTagName(self.prefix+'corrupted')
|
||||
paths = []
|
||||
for c in corrupted:
|
||||
paths.append(c.getAttribute('path'))
|
||||
c.parentNode.removeChild(c)
|
||||
c.unlink()
|
||||
return paths
|
||||
|
||||
def purge_empty_playlists(self):
|
||||
''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
|
||||
for pli in self.playlist_items():
|
||||
try:
|
||||
if not self.is_id_valid(pli.getAttribute('id')):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
except:
|
||||
continue
|
||||
for pl in self.playlists():
|
||||
empty = True
|
||||
for c in pl.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('item'):
|
||||
empty = False
|
||||
break
|
||||
if empty:
|
||||
pl.parentNode.removeChild(pl)
|
||||
pl.unlink()
|
||||
|
||||
def playlist_by_title(self, title):
|
||||
for pl in self.playlists():
|
||||
if pl.getAttribute('title').lower() == title.lower():
|
||||
return pl
|
||||
|
||||
def add_playlist(self, title):
|
||||
cid = self.max_id()+1
|
||||
pl = self.document.createElement(self.prefix+'playlist')
|
||||
pl.setAttribute('id', str(cid))
|
||||
pl.setAttribute('title', title)
|
||||
pl.setAttribute('uuid', uuid())
|
||||
self.root_element.insertBefore(pl, self.root_element.childNodes[-1])
|
||||
return pl
|
||||
|
||||
def remove_from_playlists(self, id):
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == str(id):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
tags = [t for t in tags if t]
|
||||
book.tags = tags
|
||||
self.set_playlists(book.id, tags)
|
||||
|
||||
def set_playlists(self, id, collections):
|
||||
self.remove_from_playlists(id)
|
||||
for collection in set(collections):
|
||||
coll = self.playlist_by_title(collection)
|
||||
if not coll:
|
||||
coll = self.add_playlist(collection)
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(id))
|
||||
coll.appendChild(item)
|
||||
|
||||
def get_playlists(self, bookid):
|
||||
ans = []
|
||||
for pl in self.playlists():
|
||||
for item in pl.childNodes:
|
||||
if hasattr(item, 'tagName') and item.tagName.endswith('item'):
|
||||
if item.getAttribute('id') == str(bookid):
|
||||
ans.append(pl)
|
||||
return ans
|
||||
|
||||
def next_id(self):
|
||||
return self.document.documentElement.getAttribute('nextID')
|
||||
|
||||
def set_next_id(self, id):
|
||||
self.document.documentElement.setAttribute('nextID', str(id))
|
||||
|
||||
def write(self, stream):
|
||||
""" Write XML representation of DOM tree to C{stream} """
|
||||
src = self.document.toxml('utf-8') + '\n'
|
||||
stream.write(src.replace("'", '''))
|
||||
|
||||
def book_by_id(self, id):
|
||||
for book in self:
|
||||
if str(book.id) == str(id):
|
||||
return book
|
||||
|
||||
def reorder_playlists(self):
|
||||
for title in self.tag_order.keys():
|
||||
pl = self.playlist_by_title(title)
|
||||
if not pl:
|
||||
continue
|
||||
db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')]
|
||||
pl_book_ids = [getattr(self.book_by_id(i), 'db_id', None) for i in db_ids]
|
||||
imap = {}
|
||||
for i, j in zip(pl_book_ids, db_ids):
|
||||
imap[i] = j
|
||||
pl_book_ids = [i for i in pl_book_ids if i is not None]
|
||||
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
|
||||
|
||||
if len(ordered_ids) < len(pl.childNodes):
|
||||
continue
|
||||
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
|
||||
for child in children:
|
||||
pl.removeChild(child)
|
||||
child.unlink()
|
||||
for id in ordered_ids:
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(imap[id]))
|
||||
pl.appendChild(item)
|
||||
|
||||
def fix_ids(main, carda, cardb):
|
||||
'''
|
||||
Adjust ids the XML databases.
|
||||
'''
|
||||
if hasattr(main, 'purge_empty_playlists'):
|
||||
main.purge_empty_playlists()
|
||||
if hasattr(carda, 'purge_empty_playlists'):
|
||||
carda.purge_empty_playlists()
|
||||
if hasattr(cardb, 'purge_empty_playlists'):
|
||||
cardb.purge_empty_playlists()
|
||||
|
||||
def regen_ids(db):
|
||||
if not hasattr(db, 'root_element'):
|
||||
return
|
||||
id_map = {}
|
||||
db.purge_empty_playlists()
|
||||
cid = 0 if db == main else 1
|
||||
for child in db.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute('id'):
|
||||
id_map[child.getAttribute('id')] = str(cid)
|
||||
child.setAttribute("sourceid",
|
||||
'0' if getattr(child, 'tagName', '').endswith('playlist') else '1')
|
||||
child.setAttribute('id', str(cid))
|
||||
cid += 1
|
||||
|
||||
for item in db.playlist_items():
|
||||
oid = item.getAttribute('id')
|
||||
try:
|
||||
item.setAttribute('id', id_map[oid])
|
||||
except KeyError:
|
||||
item.parentNode.removeChild(item)
|
||||
item.unlink()
|
||||
|
||||
db.reorder_playlists()
|
||||
|
||||
regen_ids(main)
|
||||
regen_ids(carda)
|
||||
regen_ids(cardb)
|
||||
|
||||
main.set_next_id(str(main.max_id()+1))
|
@ -1,54 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net> ' \
|
||||
'2009, John Schember <john at nachtimwald.com>'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Device driver for the SONY PRS-505
|
||||
Device driver for the SONY devices
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from itertools import cycle
|
||||
|
||||
from calibre.devices.usbms.cli import CLI
|
||||
from calibre.devices.usbms.device import Device
|
||||
from calibre.devices.prs505.books import BookList, fix_ids
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre.devices.prs505 import MEDIA_XML
|
||||
from calibre.devices.prs505 import CACHE_XML
|
||||
from calibre.devices.prs505.sony_cache import XMLCache
|
||||
from calibre import __appname__
|
||||
from calibre.devices.usbms.books import CollectionsBookList
|
||||
|
||||
class PRS505(CLI, Device):
|
||||
class PRS505(USBMS):
|
||||
|
||||
name = 'PRS-300/505 Device Interface'
|
||||
name = 'SONY Device Interface'
|
||||
gui_name = 'SONY Reader'
|
||||
description = _('Communicate with the Sony PRS-300/505/500 eBook reader.')
|
||||
author = 'Kovid Goyal and John Schember'
|
||||
description = _('Communicate with all the Sony eBook readers.')
|
||||
author = 'Kovid Goyal'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
path_sep = '/'
|
||||
booklist_class = CollectionsBookList
|
||||
|
||||
|
||||
FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt']
|
||||
CAN_SET_METADATA = True
|
||||
|
||||
VENDOR_ID = [0x054c] #: SONY Vendor Id
|
||||
PRODUCT_ID = [0x031e] #: Product Id for the PRS 300/505/new 500
|
||||
BCD = [0x229, 0x1000, 0x22a]
|
||||
PRODUCT_ID = [0x031e]
|
||||
BCD = [0x229, 0x1000, 0x22a, 0x31a]
|
||||
|
||||
VENDOR_NAME = 'SONY'
|
||||
WINDOWS_MAIN_MEM = re.compile('PRS-(505|300|500)')
|
||||
WINDOWS_CARD_A_MEM = re.compile(r'PRS-(505|500)[#/]\S+:MS')
|
||||
WINDOWS_CARD_B_MEM = re.compile(r'PRS-(505|500)[#/]\S+:SD')
|
||||
WINDOWS_MAIN_MEM = re.compile(
|
||||
r'(PRS-(505|300|500))|'
|
||||
r'(PRS-((700[#/])|((6|9)00&)))'
|
||||
)
|
||||
WINDOWS_CARD_A_MEM = re.compile(
|
||||
r'(PRS-(505|500)[#/]\S+:MS)|'
|
||||
r'(PRS-((700[/#]\S+:)|((6|9)00[#_]))MS)'
|
||||
)
|
||||
WINDOWS_CARD_B_MEM = re.compile(
|
||||
r'(PRS-(505|500)[#/]\S+:SD)|'
|
||||
r'(PRS-((700[/#]\S+:)|((6|9)00[#_]))SD)'
|
||||
)
|
||||
|
||||
OSX_MAIN_MEM = re.compile(r'Sony PRS-(((505|300|500)/[^:]+)|(300)) Media')
|
||||
OSX_CARD_A_MEM = re.compile(r'Sony PRS-(505|500)/[^:]+:MS Media')
|
||||
OSX_CARD_B_MEM = re.compile(r'Sony PRS-(505|500)/[^:]+:SD Media')
|
||||
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory'
|
||||
STORAGE_CARD_VOLUME_LABEL = 'Sony Reader Storage Card'
|
||||
|
||||
MEDIA_XML = 'database/cache/media.xml'
|
||||
CACHE_XML = 'Sony Reader/database/cache.xml'
|
||||
|
||||
CARD_PATH_PREFIX = __appname__
|
||||
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
@ -63,64 +66,9 @@ class PRS505(CLI, Device):
|
||||
def windows_filter_pnp_id(self, pnp_id):
|
||||
return '_LAUNCHER' in pnp_id
|
||||
|
||||
def open(self):
|
||||
self.report_progress = lambda x, y: x
|
||||
Device.open(self)
|
||||
|
||||
def write_cache(prefix):
|
||||
try:
|
||||
cachep = os.path.join(prefix, *(self.CACHE_XML.split('/')))
|
||||
if not os.path.exists(cachep):
|
||||
dname = os.path.dirname(cachep)
|
||||
if not os.path.exists(dname):
|
||||
try:
|
||||
os.makedirs(dname, mode=0777)
|
||||
except:
|
||||
time.sleep(5)
|
||||
os.makedirs(dname, mode=0777)
|
||||
with open(cachep, 'wb') as f:
|
||||
f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||
</cache>
|
||||
'''.encode('utf8'))
|
||||
return True
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if self._card_a_prefix is not None:
|
||||
if not write_cache(self._card_a_prefix):
|
||||
self._card_a_prefix = None
|
||||
if self._card_b_prefix is not None:
|
||||
if not write_cache(self._card_b_prefix):
|
||||
self._card_b_prefix = None
|
||||
|
||||
def get_device_information(self, end_session=True):
|
||||
return (self.gui_name, '', '', '')
|
||||
|
||||
def books(self, oncard=None, end_session=True):
|
||||
if oncard == 'carda' and not self._card_a_prefix:
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return []
|
||||
elif oncard == 'cardb' and not self._card_b_prefix:
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return []
|
||||
elif oncard and oncard != 'carda' and oncard != 'cardb':
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return []
|
||||
|
||||
db = self.__class__.CACHE_XML if oncard else self.__class__.MEDIA_XML
|
||||
prefix = self._card_a_prefix if oncard == 'carda' else self._card_b_prefix if oncard == 'cardb' else self._main_prefix
|
||||
bl = BookList(open(prefix + db, 'rb'), prefix, self.report_progress)
|
||||
paths = bl.purge_corrupted_files()
|
||||
for path in paths:
|
||||
path = os.path.join(prefix, path)
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return bl
|
||||
|
||||
def filename_callback(self, fname, mi):
|
||||
if getattr(mi, 'application_id', None) is not None:
|
||||
base = fname.rpartition('.')[0]
|
||||
@ -129,117 +77,43 @@ class PRS505(CLI, Device):
|
||||
fname = base + suffix + '.' + fname.rpartition('.')[-1]
|
||||
return fname
|
||||
|
||||
def upload_books(self, files, names, on_card=None, end_session=True,
|
||||
metadata=None):
|
||||
def initialize_XML_cache(self):
|
||||
paths, prefixes = {}, {}
|
||||
for prefix, path, source_id in [
|
||||
('main', MEDIA_XML, 0),
|
||||
('card_a', CACHE_XML, 1),
|
||||
('card_b', CACHE_XML, 2)
|
||||
]:
|
||||
prefix = getattr(self, '_%s_prefix'%prefix)
|
||||
if prefix is not None and os.path.exists(prefix):
|
||||
paths[source_id] = os.path.join(prefix, *(path.split('/')))
|
||||
prefixes[source_id] = prefix
|
||||
d = os.path.dirname(paths[source_id])
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
return XMLCache(paths, prefixes)
|
||||
|
||||
path = self._sanity_check(on_card, files)
|
||||
|
||||
paths, ctimes, sizes = [], [], []
|
||||
names = iter(names)
|
||||
metadata = iter(metadata)
|
||||
for i, infile in enumerate(files):
|
||||
mdata, fname = metadata.next(), names.next()
|
||||
filepath = self.create_upload_path(path, mdata, fname)
|
||||
|
||||
paths.append(filepath)
|
||||
self.put_file(infile, paths[-1], replace_file=True)
|
||||
ctimes.append(os.path.getctime(paths[-1]))
|
||||
sizes.append(os.stat(paths[-1]).st_size)
|
||||
|
||||
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
|
||||
|
||||
self.report_progress(1.0, _('Transferring books to device...'))
|
||||
|
||||
return zip(paths, sizes, ctimes, cycle([on_card]))
|
||||
|
||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||
if not locations or not metadata:
|
||||
return
|
||||
|
||||
metadata = iter(metadata)
|
||||
for location in locations:
|
||||
info = metadata.next()
|
||||
path = location[0]
|
||||
oncard = location[3]
|
||||
blist = 2 if oncard == 'cardb' else 1 if oncard == 'carda' else 0
|
||||
|
||||
if self._main_prefix and path.startswith(self._main_prefix):
|
||||
name = path.replace(self._main_prefix, '')
|
||||
elif self._card_a_prefix and path.startswith(self._card_a_prefix):
|
||||
name = path.replace(self._card_a_prefix, '')
|
||||
elif self._card_b_prefix and path.startswith(self._card_b_prefix):
|
||||
name = path.replace(self._card_b_prefix, '')
|
||||
|
||||
name = name.replace('\\', '/')
|
||||
name = name.replace('//', '/')
|
||||
if name.startswith('/'):
|
||||
name = name[1:]
|
||||
|
||||
opts = self.settings()
|
||||
collections = opts.extra_customization.split(',') if opts.extra_customization else []
|
||||
booklist = booklists[blist]
|
||||
if not hasattr(booklist, 'add_book'):
|
||||
raise ValueError(('Incorrect upload location %s. Did you choose the'
|
||||
' correct card A or B, to send books to?')%oncard)
|
||||
booklist.add_book(info, name, collections, *location[1:-1])
|
||||
fix_ids(*booklists)
|
||||
|
||||
def delete_books(self, paths, end_session=True):
|
||||
for i, path in enumerate(paths):
|
||||
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
try:
|
||||
os.removedirs(os.path.dirname(path))
|
||||
except:
|
||||
pass
|
||||
self.report_progress(1.0, _('Removing books from device...'))
|
||||
|
||||
@classmethod
|
||||
def remove_books_from_metadata(cls, paths, booklists):
|
||||
for path in paths:
|
||||
for bl in booklists:
|
||||
if hasattr(bl, 'remove_book'):
|
||||
bl.remove_book(path)
|
||||
fix_ids(*booklists)
|
||||
def books(self, oncard=None, end_session=True):
|
||||
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||
c = self.initialize_XML_cache()
|
||||
c.update_booklist(bl, {'carda':1, 'cardb':2}.get(oncard, 0))
|
||||
return bl
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
fix_ids(*booklists)
|
||||
if not os.path.exists(self._main_prefix):
|
||||
os.makedirs(self._main_prefix)
|
||||
with open(self._main_prefix + self.__class__.MEDIA_XML, 'wb') as f:
|
||||
booklists[0].write(f)
|
||||
c = self.initialize_XML_cache()
|
||||
blists = {}
|
||||
for i in c.paths:
|
||||
if booklists[i] is not None:
|
||||
blists[i] = booklists[i]
|
||||
opts = self.settings()
|
||||
collections = ['series', 'tags']
|
||||
if opts.extra_customization:
|
||||
collections = [x.strip() for x in
|
||||
opts.extra_customization.split(',')]
|
||||
|
||||
def write_card_prefix(prefix, listid):
|
||||
if prefix is not None and hasattr(booklists[listid], 'write'):
|
||||
tgt = os.path.join(prefix, *(self.CACHE_XML.split('/')))
|
||||
base = os.path.dirname(tgt)
|
||||
if not os.path.exists(base):
|
||||
os.makedirs(base)
|
||||
with open(tgt, 'wb') as f:
|
||||
booklists[listid].write(f)
|
||||
write_card_prefix(self._card_a_prefix, 1)
|
||||
write_card_prefix(self._card_b_prefix, 2)
|
||||
c.update(blists, collections)
|
||||
c.write()
|
||||
|
||||
self.report_progress(1.0, _('Sending metadata to device...'))
|
||||
|
||||
|
||||
class PRS700(PRS505):
|
||||
|
||||
name = 'PRS-600/700/900 Device Interface'
|
||||
description = _('Communicate with the Sony PRS-600/700/900 eBook reader.')
|
||||
author = 'Kovid Goyal and John Schember'
|
||||
gui_name = 'SONY Reader'
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
||||
BCD = [0x31a]
|
||||
|
||||
WINDOWS_MAIN_MEM = re.compile('PRS-((700[#/])|((6|9)00&))')
|
||||
WINDOWS_CARD_A_MEM = re.compile(r'PRS-((700[/#]\S+:)|((6|9)00[#_]))MS')
|
||||
WINDOWS_CARD_B_MEM = re.compile(r'PRS-((700[/#]\S+:)|((6|9)00[#_]))SD')
|
||||
|
||||
OSX_MAIN_MEM = re.compile(r'Sony PRS-((700/[^:]+)|((6|9)00)) Media')
|
||||
OSX_CARD_A_MEM = re.compile(r'Sony PRS-((700/[^:]+:)|((6|9)00 ))MS Media')
|
||||
OSX_CARD_B_MEM = re.compile(r'Sony PRS-((700/[^:]+:)|((6|9)00 ))SD Media')
|
||||
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
||||
|
||||
|
||||
|
523
src/calibre/devices/prs505/sony_cache.py
Normal file
@ -0,0 +1,523 @@
|
||||
#!/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 os, time
|
||||
from pprint import pprint
|
||||
from base64 import b64decode
|
||||
from uuid import uuid4
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre import prints, guess_type
|
||||
from calibre.devices.errors import DeviceError
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.metadata import authors_to_string, title_sort
|
||||
|
||||
# Utility functions {{{
|
||||
EMPTY_CARD_CACHE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<cache xmlns="http://www.kinoma.com/FskCache/1">
|
||||
</cache>
|
||||
'''
|
||||
|
||||
MIME_MAP = {
|
||||
"lrf" : "application/x-sony-bbeb",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
"rtf" : "application/rtf",
|
||||
"pdf" : "application/pdf",
|
||||
"txt" : "text/plain" ,
|
||||
'epub': 'application/epub+zip',
|
||||
}
|
||||
|
||||
DAY_MAP = dict(Sun=0, Mon=1, Tue=2, Wed=3, Thu=4, Fri=5, Sat=6)
|
||||
MONTH_MAP = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, Oct=10, Nov=11, Dec=12)
|
||||
INVERSE_DAY_MAP = dict(zip(DAY_MAP.values(), DAY_MAP.keys()))
|
||||
INVERSE_MONTH_MAP = dict(zip(MONTH_MAP.values(), MONTH_MAP.keys()))
|
||||
|
||||
def strptime(src):
|
||||
src = src.strip()
|
||||
src = src.split()
|
||||
src[0] = str(DAY_MAP[src[0][:-1]])+','
|
||||
src[2] = str(MONTH_MAP[src[2]])
|
||||
return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z')
|
||||
|
||||
def strftime(epoch, zone=time.gmtime):
|
||||
src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split()
|
||||
src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+','
|
||||
src[2] = INVERSE_MONTH_MAP[int(src[2])]
|
||||
return ' '.join(src)
|
||||
|
||||
def uuid():
|
||||
return str(uuid4()).replace('-', '', 1).upper()
|
||||
|
||||
# }}}
|
||||
|
||||
class XMLCache(object):
|
||||
|
||||
def __init__(self, paths, prefixes):
|
||||
if DEBUG:
|
||||
prints('Building XMLCache...')
|
||||
pprint(paths)
|
||||
self.paths = paths
|
||||
self.prefixes = prefixes
|
||||
|
||||
# Parse XML files {{{
|
||||
parser = etree.XMLParser(recover=True)
|
||||
self.roots = {}
|
||||
for source_id, path in paths.items():
|
||||
if source_id == 0:
|
||||
if not os.path.exists(path):
|
||||
raise DeviceError('The SONY XML cache media.xml does not exist. Try'
|
||||
' disconnecting and reconnecting your reader.')
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
else:
|
||||
raw = EMPTY_CARD_CACHE
|
||||
if os.access(path, os.R_OK):
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read()
|
||||
self.roots[source_id] = etree.fromstring(xml_to_unicode(
|
||||
raw, strip_encoding_pats=True, assume_utf8=True,
|
||||
verbose=DEBUG)[0],
|
||||
parser=parser)
|
||||
# }}}
|
||||
|
||||
recs = self.roots[0].xpath('//*[local-name()="records"]')
|
||||
if not recs:
|
||||
raise DeviceError('The SONY XML database is corrupted (no'
|
||||
' <records>). Try disconnecting an reconnecting'
|
||||
' your reader.')
|
||||
self.record_roots = {}
|
||||
self.record_roots.update(self.roots)
|
||||
self.record_roots[0] = recs[0]
|
||||
|
||||
self.detect_namespaces()
|
||||
|
||||
|
||||
# Playlist management {{{
|
||||
def purge_broken_playlist_items(self, root):
|
||||
for pl in root.xpath('//*[local-name()="playlist"]'):
|
||||
seen = set([])
|
||||
for item in list(pl):
|
||||
id_ = item.get('id', None)
|
||||
if id_ is None or id_ in seen or not root.xpath(
|
||||
'//*[local-name()!="item" and @id="%s"]'%id_):
|
||||
if DEBUG:
|
||||
if id_ is None:
|
||||
cause = 'invalid id'
|
||||
elif id_ in seen:
|
||||
cause = 'duplicate item'
|
||||
else:
|
||||
cause = 'id not found'
|
||||
prints('Purging broken playlist item:',
|
||||
id_, 'from playlist:', pl.get('title', None),
|
||||
'because:', cause)
|
||||
item.getparent().remove(item)
|
||||
continue
|
||||
seen.add(id_)
|
||||
|
||||
def prune_empty_playlists(self):
|
||||
for i, root in self.record_roots.items():
|
||||
self.purge_broken_playlist_items(root)
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
if len(playlist) == 0 or not playlist.get('title', None):
|
||||
if DEBUG:
|
||||
prints('Removing playlist id:', playlist.get('id', None),
|
||||
playlist.get('title', None))
|
||||
playlist.getparent().remove(playlist)
|
||||
|
||||
def ensure_unique_playlist_titles(self):
|
||||
for i, root in self.record_roots.items():
|
||||
seen = set([])
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
title = playlist.get('title', None)
|
||||
if title is None:
|
||||
title = _('Unnamed')
|
||||
playlist.set('title', title)
|
||||
if title in seen:
|
||||
for i in range(2, 1000):
|
||||
if title+str(i) not in seen:
|
||||
title = title+str(i)
|
||||
playlist.set('title', title)
|
||||
break
|
||||
else:
|
||||
seen.add(title)
|
||||
|
||||
def get_playlist_map(self):
|
||||
ans = {}
|
||||
self.ensure_unique_playlist_titles()
|
||||
self.prune_empty_playlists()
|
||||
for i, root in self.record_roots.items():
|
||||
ans[i] = []
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
items = []
|
||||
for item in playlist:
|
||||
id_ = item.get('id', None)
|
||||
records = root.xpath(
|
||||
'//*[local-name()="text" and @id="%s"]'%id_)
|
||||
if records:
|
||||
items.append(records[0])
|
||||
ans[i].append((playlist.get('title'), items))
|
||||
return ans
|
||||
|
||||
def get_or_create_playlist(self, bl_idx, title):
|
||||
root = self.record_roots[bl_idx]
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
if playlist.get('title', None) == title:
|
||||
return playlist
|
||||
if DEBUG:
|
||||
prints('Creating playlist:', title)
|
||||
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
|
||||
nsmap=root.nsmap, attrib={
|
||||
'uuid' : uuid(),
|
||||
'title': title,
|
||||
'id' : str(self.max_id(root)+1),
|
||||
'sourceid': '1'
|
||||
})
|
||||
root.append(ans)
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
def fix_ids(self): # {{{
|
||||
if DEBUG:
|
||||
prints('Running fix_ids()')
|
||||
|
||||
def ensure_numeric_ids(root):
|
||||
idmap = {}
|
||||
for x in root.xpath('child::*[@id]'):
|
||||
id_ = x.get('id')
|
||||
try:
|
||||
id_ = int(id_)
|
||||
except:
|
||||
x.set('id', '-1')
|
||||
idmap[id_] = '-1'
|
||||
|
||||
if DEBUG and idmap:
|
||||
prints('Found non numeric ids:')
|
||||
prints(list(idmap.keys()))
|
||||
return idmap
|
||||
|
||||
def remap_playlist_references(root, idmap):
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
for item in playlist.xpath(
|
||||
'descendant::*[@id and local-name()="item"]'):
|
||||
id_ = item.get('id')
|
||||
if id_ in idmap:
|
||||
item.set('id', idmap[id_])
|
||||
if DEBUG:
|
||||
prints('Remapping id %s to %s'%(id_, idmap[id_]))
|
||||
|
||||
def ensure_media_xml_base_ids(root):
|
||||
for num, tag in enumerate(('library', 'watchSpecial')):
|
||||
for x in root.xpath('//*[local-name()="%s"]'%tag):
|
||||
x.set('id', str(num))
|
||||
|
||||
def rebase_ids(root, base, sourceid, pl_sourceid):
|
||||
'Rebase all ids and also make them consecutive'
|
||||
for item in root.xpath('//*[@sourceid]'):
|
||||
sid = pl_sourceid if item.tag.endswith('playlist') else sourceid
|
||||
item.set('sourceid', str(sid))
|
||||
# Only rebase ids of nodes that are immediate children of the
|
||||
# record root (that way playlist/itemnodes are unaffected
|
||||
items = root.xpath('child::*[@id]')
|
||||
items.sort(cmp=lambda x,y:cmp(int(x.get('id')), int(y.get('id'))))
|
||||
idmap = {}
|
||||
for i, item in enumerate(items):
|
||||
old = int(item.get('id'))
|
||||
new = base + i
|
||||
if old != new:
|
||||
item.set('id', str(new))
|
||||
idmap[str(old)] = str(new)
|
||||
return idmap
|
||||
|
||||
self.prune_empty_playlists()
|
||||
|
||||
for i in sorted(self.roots.keys()):
|
||||
root = self.record_roots[i]
|
||||
if i == 0:
|
||||
ensure_media_xml_base_ids(root)
|
||||
|
||||
idmap = ensure_numeric_ids(root)
|
||||
remap_playlist_references(root, idmap)
|
||||
if i == 0:
|
||||
sourceid, playlist_sid = 1, 0
|
||||
base = 0
|
||||
else:
|
||||
previous = i-1
|
||||
if previous not in self.roots:
|
||||
previous = 0
|
||||
max_id = self.max_id(self.roots[previous])
|
||||
sourceid = playlist_sid = max_id + 1
|
||||
base = max_id + 2
|
||||
idmap = rebase_ids(root, base, sourceid, playlist_sid)
|
||||
remap_playlist_references(root, idmap)
|
||||
|
||||
last_bl = max(self.roots.keys())
|
||||
max_id = self.max_id(self.roots[last_bl])
|
||||
self.roots[0].set('nextID', str(max_id+1))
|
||||
# }}}
|
||||
|
||||
# Update JSON from XML {{{
|
||||
def update_booklist(self, bl, bl_index):
|
||||
if bl_index not in self.record_roots:
|
||||
return
|
||||
if DEBUG:
|
||||
prints('Updating JSON cache:', bl_index)
|
||||
root = self.record_roots[bl_index]
|
||||
pmap = self.get_playlist_map()[bl_index]
|
||||
playlist_map = {}
|
||||
for title, records in pmap:
|
||||
for record in records:
|
||||
path = record.get('path', None)
|
||||
if path:
|
||||
if path not in playlist_map:
|
||||
playlist_map[path] = []
|
||||
playlist_map[path].append(title)
|
||||
|
||||
for book in bl:
|
||||
record = self.book_by_lpath(book.lpath, root)
|
||||
if record is not None:
|
||||
title = record.get('title', None)
|
||||
if title is not None and title != book.title:
|
||||
if DEBUG:
|
||||
prints('Renaming title', book.title, 'to', title)
|
||||
book.title = title
|
||||
# We shouldn't do this for Sonys, because the reader strips
|
||||
# all but the first author.
|
||||
# authors = record.get('author', None)
|
||||
# if authors is not None:
|
||||
# authors = string_to_authors(authors)
|
||||
# if authors != book.authors:
|
||||
# if DEBUG:
|
||||
# prints('Renaming authors', book.authors, 'to',
|
||||
# authors)
|
||||
# book.authors = authors
|
||||
for thumbnail in record.xpath(
|
||||
'descendant::*[local-name()="thumbnail"]'):
|
||||
for img in thumbnail.xpath(
|
||||
'descendant::*[local-name()="jpeg"]|'
|
||||
'descendant::*[local-name()="png"]'):
|
||||
if img.text:
|
||||
raw = b64decode(img.text.strip())
|
||||
book.thumbnail = raw
|
||||
break
|
||||
break
|
||||
if book.lpath in playlist_map:
|
||||
tags = playlist_map[book.lpath]
|
||||
book.device_collections = tags
|
||||
|
||||
# }}}
|
||||
|
||||
# Update XML from JSON {{{
|
||||
def update(self, booklists, collections_attributes):
|
||||
playlist_map = self.get_playlist_map()
|
||||
|
||||
for i, booklist in booklists.items():
|
||||
if DEBUG:
|
||||
prints('Updating XML Cache:', i)
|
||||
root = self.record_roots[i]
|
||||
for book in booklist:
|
||||
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
||||
record = self.book_by_lpath(book.lpath, root)
|
||||
if record is None:
|
||||
record = self.create_text_record(root, i, book.lpath)
|
||||
self.update_text_record(record, book, path, i)
|
||||
|
||||
bl_pmap = playlist_map[i]
|
||||
self.update_playlists(i, root, booklist, bl_pmap,
|
||||
collections_attributes)
|
||||
|
||||
self.fix_ids()
|
||||
|
||||
# This is needed to update device_collections
|
||||
for i, booklist in booklists.items():
|
||||
self.update_booklist(booklist, i)
|
||||
|
||||
def update_playlists(self, bl_index, root, booklist, playlist_map,
|
||||
collections_attributes):
|
||||
collections = booklist.get_collections(collections_attributes)
|
||||
for category, books in collections.items():
|
||||
records = [self.book_by_lpath(b.lpath, root) for b in books]
|
||||
# Remove any books that were not found, although this
|
||||
# *should* never happen
|
||||
if DEBUG and None in records:
|
||||
prints('WARNING: Some elements in the JSON cache were not'
|
||||
' found in the XML cache')
|
||||
records = [x for x in records if x is not None]
|
||||
for rec in records:
|
||||
if rec.get('id', None) is None:
|
||||
rec.set('id', str(self.max_id(root)+1))
|
||||
ids = [x.get('id', None) for x in records]
|
||||
if None in ids:
|
||||
if DEBUG:
|
||||
prints('WARNING: Some <text> elements do not have ids')
|
||||
ids = [x for x in ids if x is not None]
|
||||
|
||||
playlist = self.get_or_create_playlist(bl_index, category)
|
||||
playlist_ids = []
|
||||
for item in playlist:
|
||||
id_ = item.get('id', None)
|
||||
if id_ is not None:
|
||||
playlist_ids.append(id_)
|
||||
for item in list(playlist):
|
||||
playlist.remove(item)
|
||||
|
||||
extra_ids = [x for x in playlist_ids if x not in ids]
|
||||
for id_ in ids + extra_ids:
|
||||
item = playlist.makeelement(
|
||||
'{%s}item'%self.namespaces[bl_index],
|
||||
nsmap=playlist.nsmap, attrib={'id':id_})
|
||||
playlist.append(item)
|
||||
|
||||
# Delete playlist entries not in collections
|
||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||
title = playlist.get('title', None)
|
||||
if title not in collections:
|
||||
if DEBUG:
|
||||
prints('Deleting playlist:', playlist.get('title', ''))
|
||||
playlist.getparent().remove(playlist)
|
||||
continue
|
||||
books = collections[title]
|
||||
records = [self.book_by_lpath(b.lpath, root) for b in books]
|
||||
records = [x for x in records if x is not None]
|
||||
ids = [x.get('id', None) for x in records]
|
||||
ids = [x for x in ids if x is not None]
|
||||
for item in list(playlist):
|
||||
if item.get('id', None) not in ids:
|
||||
if DEBUG:
|
||||
prints('Deleting item:', item.get('id', ''),
|
||||
'from playlist:', playlist.get('title', ''))
|
||||
playlist.remove(item)
|
||||
|
||||
def create_text_record(self, root, bl_id, lpath):
|
||||
namespace = self.namespaces[bl_id]
|
||||
id_ = self.max_id(root)+1
|
||||
attrib = {
|
||||
'page':'0', 'part':'0','pageOffset':'0','scale':'0',
|
||||
'id':str(id_), 'sourceid':'1', 'path':lpath}
|
||||
ans = root.makeelement('{%s}text'%namespace, attrib=attrib, nsmap=root.nsmap)
|
||||
root.append(ans)
|
||||
return ans
|
||||
|
||||
def update_text_record(self, record, book, path, bl_index):
|
||||
timestamp = os.path.getctime(path)
|
||||
date = strftime(timestamp)
|
||||
if date != record.get('date', None):
|
||||
if DEBUG:
|
||||
prints('Changing date of', path, 'from',
|
||||
record.get('date', ''), 'to', date)
|
||||
prints('\tctime', strftime(os.path.getctime(path)))
|
||||
prints('\tmtime', strftime(os.path.getmtime(path)))
|
||||
record.set('date', date)
|
||||
record.set('size', str(os.stat(path).st_size))
|
||||
record.set('title', book.title)
|
||||
ts = book.title_sort
|
||||
if not ts:
|
||||
ts = title_sort(book.title)
|
||||
record.set('titleSorter', ts)
|
||||
record.set('author', authors_to_string(book.authors))
|
||||
ext = os.path.splitext(path)[1]
|
||||
if ext:
|
||||
ext = ext[1:].lower()
|
||||
mime = MIME_MAP.get(ext, None)
|
||||
if mime is None:
|
||||
mime = guess_type('a.'+ext)[0]
|
||||
if mime is not None:
|
||||
record.set('mime', mime)
|
||||
if 'sourceid' not in record.attrib:
|
||||
record.set('sourceid', '1')
|
||||
if 'id' not in record.attrib:
|
||||
num = self.max_id(record.getroottree().getroot())
|
||||
record.set('id', str(num+1))
|
||||
# }}}
|
||||
|
||||
# Writing the XML files {{{
|
||||
def cleanup_whitespace(self, bl_index):
|
||||
root = self.record_roots[bl_index]
|
||||
level = 2 if bl_index == 0 else 1
|
||||
if len(root) > 0:
|
||||
root.text = '\n'+'\t'*level
|
||||
for child in root:
|
||||
child.tail = '\n'+'\t'*level
|
||||
if len(child) > 0:
|
||||
child.text = '\n'+'\t'*(level+1)
|
||||
for gc in child:
|
||||
gc.tail = '\n'+'\t'*(level+1)
|
||||
child.iterchildren(reversed=True).next().tail = '\n'+'\t'*level
|
||||
root.iterchildren(reversed=True).next().tail = '\n'+'\t'*(level-1)
|
||||
|
||||
def move_playlists_to_bottom(self):
|
||||
for root in self.record_roots.values():
|
||||
seen = []
|
||||
for pl in root.xpath('//*[local-name()="playlist"]'):
|
||||
pl.getparent().remove(pl)
|
||||
seen.append(pl)
|
||||
for pl in seen:
|
||||
root.append(pl)
|
||||
|
||||
|
||||
def write(self):
|
||||
for i, path in self.paths.items():
|
||||
self.move_playlists_to_bottom()
|
||||
self.cleanup_whitespace(i)
|
||||
raw = etree.tostring(self.roots[i], encoding='UTF-8',
|
||||
xml_declaration=True)
|
||||
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 {{{
|
||||
def book_by_lpath(self, lpath, root):
|
||||
matches = root.xpath(u'//*[local-name()="text" and @path="%s"]'%lpath)
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
|
||||
def max_id(self, root):
|
||||
ans = -1
|
||||
for x in root.xpath('//*[@id]'):
|
||||
id_ = x.get('id')
|
||||
try:
|
||||
num = int(id_)
|
||||
if num > ans:
|
||||
ans = num
|
||||
except:
|
||||
continue
|
||||
return ans
|
||||
|
||||
def detect_namespaces(self):
|
||||
self.nsmaps = {}
|
||||
for i, root in self.roots.items():
|
||||
self.nsmaps[i] = root.nsmap
|
||||
|
||||
self.namespaces = {}
|
||||
for i in self.roots:
|
||||
for c in ('library', 'text', 'image', 'playlist', 'thumbnail',
|
||||
'watchSpecial'):
|
||||
matches = self.record_roots[i].xpath('//*[local-name()="%s"]'%c)
|
||||
if matches:
|
||||
e = matches[0]
|
||||
self.namespaces[i] = e.nsmap[e.prefix]
|
||||
break
|
||||
if i not in self.namespaces:
|
||||
ns = self.nsmaps[i].get(None, None)
|
||||
for prefix in self.nsmaps[i]:
|
||||
if prefix is not None:
|
||||
ns = self.nsmaps[i][prefix]
|
||||
break
|
||||
self.namespaces[i] = ns
|
||||
|
||||
if DEBUG:
|
||||
prints('Found nsmaps:')
|
||||
pprint(self.nsmaps)
|
||||
prints('Found namespaces:')
|
||||
pprint(self.namespaces)
|
||||
# }}}
|
||||
|
@ -4,29 +4,59 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import os, re, time, sys
|
||||
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.devices.mime import mime_type_ext
|
||||
from calibre.devices.interface import BookList as _BookList
|
||||
from calibre.constants import filesystem_encoding, preferred_encoding
|
||||
from calibre import isbytestring
|
||||
|
||||
class Book(object):
|
||||
class Book(MetaInformation):
|
||||
|
||||
def __init__(self, path, title, authors, mime):
|
||||
self.title = title
|
||||
self.authors = authors
|
||||
self.mime = mime
|
||||
self.size = os.path.getsize(path)
|
||||
BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections']
|
||||
|
||||
JSON_ATTRS = [
|
||||
'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort',
|
||||
'title_sort', 'comments', 'category', 'publisher', 'series',
|
||||
'series_index', 'rating', 'isbn', 'language', 'application_id',
|
||||
'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type',
|
||||
'uuid',
|
||||
]
|
||||
|
||||
def __init__(self, prefix, lpath, size=None, other=None):
|
||||
from calibre.ebooks.metadata.meta import path_to_ext
|
||||
|
||||
MetaInformation.__init__(self, '')
|
||||
|
||||
self.device_collections = []
|
||||
self.path = os.path.join(prefix, lpath)
|
||||
if os.sep == '\\':
|
||||
self.path = self.path.replace('/', '\\')
|
||||
self.lpath = lpath.replace('\\', '/')
|
||||
else:
|
||||
self.lpath = lpath
|
||||
self.mime = mime_type_ext(path_to_ext(lpath))
|
||||
self.size = size # will be set later if None
|
||||
try:
|
||||
self.datetime = time.gmtime(os.path.getctime(path))
|
||||
except ValueError:
|
||||
self.datetime = time.gmtime(os.path.getctime(self.path))
|
||||
except:
|
||||
self.datetime = time.gmtime()
|
||||
self.path = path
|
||||
self.thumbnail = None
|
||||
self.tags = []
|
||||
if other:
|
||||
self.smart_update(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.path == other.path
|
||||
return self.path == getattr(other, 'path', None)
|
||||
|
||||
@dynamic_property
|
||||
def db_id(self):
|
||||
doc = '''The database id in the application database that this file corresponds to'''
|
||||
def fget(self):
|
||||
match = re.search(r'_(\d+)$', self.lpath.rpartition('.')[0])
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
@dynamic_property
|
||||
def title_sorter(self):
|
||||
@ -39,18 +69,90 @@ class Book(object):
|
||||
def thumbnail(self):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
def smart_update(self, other):
|
||||
'''
|
||||
Return a utf-8 encoded string with title author and path information
|
||||
Merge the information in C{other} into self. In case of conflicts, the information
|
||||
in C{other} takes precedence, unless the information in C{other} is NULL.
|
||||
'''
|
||||
return self.title.encode('utf-8') + " by " + \
|
||||
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
|
||||
|
||||
MetaInformation.smart_update(self, other)
|
||||
|
||||
for attr in self.BOOK_ATTRS:
|
||||
if hasattr(other, attr):
|
||||
val = getattr(other, attr, None)
|
||||
setattr(self, attr, val)
|
||||
|
||||
def to_json(self):
|
||||
json = {}
|
||||
for attr in self.JSON_ATTRS:
|
||||
val = getattr(self, attr)
|
||||
if isbytestring(val):
|
||||
enc = filesystem_encoding if attr == 'lpath' else preferred_encoding
|
||||
val = val.decode(enc, 'replace')
|
||||
elif isinstance(val, (list, tuple)):
|
||||
val = [x.decode(preferred_encoding, 'replace') if
|
||||
isbytestring(x) else x for x in val]
|
||||
json[attr] = val
|
||||
return json
|
||||
|
||||
class BookList(_BookList):
|
||||
|
||||
def supports_tags(self):
|
||||
def supports_collections(self):
|
||||
return False
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
pass
|
||||
def add_book(self, book, replace_metadata):
|
||||
if book not in self:
|
||||
self.append(book)
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_book(self, book):
|
||||
self.remove(book)
|
||||
|
||||
def get_collections(self):
|
||||
return {}
|
||||
|
||||
|
||||
class CollectionsBookList(BookList):
|
||||
|
||||
def supports_collections(self):
|
||||
return True
|
||||
|
||||
def get_collections(self, collection_attributes):
|
||||
collections = {}
|
||||
series_categories = set([])
|
||||
collection_attributes = list(collection_attributes)+['device_collections']
|
||||
for attr in collection_attributes:
|
||||
attr = attr.strip()
|
||||
for book in self:
|
||||
val = getattr(book, attr, None)
|
||||
if not val: continue
|
||||
if isbytestring(val):
|
||||
val = val.decode(preferred_encoding, 'replace')
|
||||
if isinstance(val, (list, tuple)):
|
||||
val = list(val)
|
||||
elif isinstance(val, unicode):
|
||||
val = [val]
|
||||
for category in val:
|
||||
if attr == 'tags' and len(category) > 1 and \
|
||||
category[0] == '[' and category[-1] == ']':
|
||||
continue
|
||||
if category not in collections:
|
||||
collections[category] = []
|
||||
if book not in collections[category]:
|
||||
collections[category].append(book)
|
||||
if attr == 'series':
|
||||
series_categories.add(category)
|
||||
|
||||
# Sort collections
|
||||
for category, books in collections.items():
|
||||
def tgetter(x):
|
||||
return getattr(x, 'title_sort', 'zzzz')
|
||||
books.sort(cmp=lambda x,y:cmp(tgetter(x), tgetter(y)))
|
||||
if category in series_categories:
|
||||
# Ensures books are sub sorted by title
|
||||
def getter(x):
|
||||
return getattr(x, 'series_index', sys.maxint)
|
||||
books.sort(cmp=lambda x,y:cmp(getter(x), getter(y)))
|
||||
return collections
|
||||
|
||||
|
@ -109,15 +109,16 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
def _windows_space(cls, prefix):
|
||||
if not prefix:
|
||||
return 0, 0
|
||||
prefix = prefix[:-1]
|
||||
win32file = __import__('win32file', globals(), locals(), [], -1)
|
||||
try:
|
||||
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
|
||||
win32file.GetDiskFreeSpace(prefix[:-1])
|
||||
win32file.GetDiskFreeSpace(prefix)
|
||||
except Exception, err:
|
||||
if getattr(err, 'args', [None])[0] == 21: # Disk not ready
|
||||
time.sleep(3)
|
||||
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
|
||||
win32file.GetDiskFreeSpace(prefix[:-1])
|
||||
win32file.GetDiskFreeSpace(prefix)
|
||||
else: raise
|
||||
mult = sectors_per_cluster * bytes_per_sector
|
||||
return total_clusters * mult, free_clusters * mult
|
||||
@ -827,7 +828,7 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
if not isinstance(template, unicode):
|
||||
template = template.decode('utf-8')
|
||||
app_id = str(getattr(mdata, 'application_id', ''))
|
||||
# The SONY readers need to have the db id in the created filename
|
||||
# The db id will be in the created filename
|
||||
extra_components = get_components(template, mdata, fname,
|
||||
length=250-len(app_id)-1)
|
||||
if not extra_components:
|
||||
@ -836,6 +837,9 @@ class Device(DeviceConfig, DevicePlugin):
|
||||
else:
|
||||
extra_components[-1] = sanitize(self.filename_callback(extra_components[-1]+ext, mdata))
|
||||
|
||||
if extra_components[-1] and extra_components[-1][0] in ('.', '_'):
|
||||
extra_components[-1] = 'x' + extra_components[-1][1:]
|
||||
|
||||
if special_tag is not None:
|
||||
name = extra_components[-1]
|
||||
extra_components = []
|
||||
|
@ -11,15 +11,15 @@ for a particular device.
|
||||
'''
|
||||
|
||||
import os
|
||||
import fnmatch
|
||||
import re
|
||||
import json
|
||||
from itertools import cycle
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre import prints, isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.devices.usbms.cli import CLI
|
||||
from calibre.devices.usbms.device import Device
|
||||
from calibre.devices.usbms.books import BookList, Book
|
||||
from calibre.devices.mime import mime_type_ext
|
||||
|
||||
# CLI must come before Device as it implements the CLI functions that
|
||||
# are inherited from the device interface in Device.
|
||||
@ -29,8 +29,16 @@ class USBMS(CLI, Device):
|
||||
author = _('John Schember')
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
||||
# Store type instances of BookList and Book. We must do this because
|
||||
# a) we need to override these classes in some device drivers, and
|
||||
# b) the classmethods seem only to see real attributes declared in the
|
||||
# class, not attributes stored in the class
|
||||
booklist_class = BookList
|
||||
book_class = Book
|
||||
|
||||
FORMATS = []
|
||||
CAN_SET_METADATA = False
|
||||
METADATA_CACHE = 'metadata.calibre'
|
||||
|
||||
def get_device_information(self, end_session=True):
|
||||
self.report_progress(1.0, _('Get device information...'))
|
||||
@ -38,61 +46,111 @@ class USBMS(CLI, Device):
|
||||
|
||||
def books(self, oncard=None, end_session=True):
|
||||
from calibre.ebooks.metadata.meta import path_to_ext
|
||||
bl = BookList()
|
||||
|
||||
dummy_bl = BookList(None, None, None)
|
||||
|
||||
if oncard == 'carda' and not self._card_a_prefix:
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return bl
|
||||
return dummy_bl
|
||||
elif oncard == 'cardb' and not self._card_b_prefix:
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return bl
|
||||
return dummy_bl
|
||||
elif oncard and oncard != 'carda' and oncard != 'cardb':
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
return bl
|
||||
return dummy_bl
|
||||
|
||||
prefix = self._card_a_prefix if oncard == 'carda' else \
|
||||
self._card_b_prefix if oncard == 'cardb' \
|
||||
else self._main_prefix
|
||||
|
||||
prefix = self._card_a_prefix if oncard == 'carda' else self._card_b_prefix if oncard == 'cardb' else self._main_prefix
|
||||
ebook_dirs = self.EBOOK_DIR_CARD_A if oncard == 'carda' else \
|
||||
self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \
|
||||
self.get_main_ebook_dir()
|
||||
|
||||
if isinstance(ebook_dirs, basestring):
|
||||
ebook_dirs = [ebook_dirs]
|
||||
for ebook_dir in ebook_dirs:
|
||||
ebook_dir = os.path.join(prefix, *(ebook_dir.split('/'))) if ebook_dir else prefix
|
||||
if not os.path.exists(ebook_dir): continue
|
||||
# Get all books in the ebook_dir directory
|
||||
if self.SUPPORTS_SUB_DIRS:
|
||||
for path, dirs, files in os.walk(ebook_dir):
|
||||
# Filter out anything that isn't in the list of supported ebook types
|
||||
for book_type in self.FORMATS:
|
||||
match = fnmatch.filter(files, '*.%s' % (book_type))
|
||||
for i, filename in enumerate(match):
|
||||
self.report_progress((i+1) / float(len(match)), _('Getting list of books on device...'))
|
||||
# get the metadata cache
|
||||
bl = self.booklist_class(oncard, prefix, self.settings)
|
||||
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
|
||||
|
||||
# make a dict cache of paths so the lookup in the loop below is faster.
|
||||
bl_cache = {}
|
||||
for idx,b in enumerate(bl):
|
||||
bl_cache[b.lpath] = idx
|
||||
|
||||
def update_booklist(filename, path, prefix):
|
||||
changed = False
|
||||
if path_to_ext(filename) in self.FORMATS:
|
||||
try:
|
||||
bl.append(self.__class__.book_from_path(os.path.join(path, filename)))
|
||||
lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2]
|
||||
if lpath.startswith(os.sep):
|
||||
lpath = lpath[len(os.sep):]
|
||||
lpath = lpath.replace('\\', '/')
|
||||
idx = bl_cache.get(lpath, None)
|
||||
if idx is not None:
|
||||
bl_cache[lpath] = None
|
||||
if self.update_metadata_item(bl[idx]):
|
||||
#print 'update_metadata_item returned true'
|
||||
changed = True
|
||||
else:
|
||||
if bl.add_book(self.book_from_path(prefix, lpath),
|
||||
replace_metadata=False):
|
||||
changed = True
|
||||
except: # Probably a filename encoding error
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
return changed
|
||||
if isinstance(ebook_dirs, basestring):
|
||||
ebook_dirs = [ebook_dirs]
|
||||
for ebook_dir in ebook_dirs:
|
||||
ebook_dir = self.path_to_unicode(ebook_dir)
|
||||
ebook_dir = self.normalize_path( \
|
||||
os.path.join(prefix, *(ebook_dir.split('/'))) \
|
||||
if ebook_dir else prefix)
|
||||
if not os.path.exists(ebook_dir): continue
|
||||
# Get all books in the ebook_dir directory
|
||||
if self.SUPPORTS_SUB_DIRS:
|
||||
# build a list of files to check, so we can accurately report progress
|
||||
flist = []
|
||||
for path, dirs, files in os.walk(ebook_dir):
|
||||
for filename in files:
|
||||
if filename != self.METADATA_CACHE:
|
||||
flist.append({'filename': self.path_to_unicode(filename),
|
||||
'path':self.path_to_unicode(path)})
|
||||
for i, f in enumerate(flist):
|
||||
self.report_progress(i/float(len(flist)), _('Getting list of books on device...'))
|
||||
changed = update_booklist(f['filename'], f['path'], prefix)
|
||||
if changed:
|
||||
need_sync = True
|
||||
else:
|
||||
paths = os.listdir(ebook_dir)
|
||||
for i, filename in enumerate(paths):
|
||||
self.report_progress((i+1) / float(len(paths)), _('Getting list of books on device...'))
|
||||
if path_to_ext(filename) in self.FORMATS:
|
||||
try:
|
||||
bl.append(self.__class__.book_from_path(os.path.join(ebook_dir, filename)))
|
||||
except: # Probably a file name encoding error
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
changed = update_booklist(self.path_to_unicode(filename), ebook_dir, prefix)
|
||||
if changed:
|
||||
need_sync = True
|
||||
|
||||
# Remove books that are no longer in the filesystem. Cache contains
|
||||
# indices into the booklist if book not in filesystem, None otherwise
|
||||
# Do the operation in reverse order so indices remain valid
|
||||
for idx in sorted(bl_cache.itervalues(), reverse=True):
|
||||
if idx is not None:
|
||||
need_sync = True
|
||||
del bl[idx]
|
||||
|
||||
#print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \
|
||||
# (len(bl_cache), len(bl), need_sync)
|
||||
if need_sync: #self.count_found_in_bl != len(bl) or need_sync:
|
||||
if oncard == 'cardb':
|
||||
self.sync_booklists((None, None, bl))
|
||||
elif oncard == 'carda':
|
||||
self.sync_booklists((None, bl, None))
|
||||
else:
|
||||
self.sync_booklists((bl, None, None))
|
||||
|
||||
self.report_progress(1.0, _('Getting list of books on device...'))
|
||||
|
||||
return bl
|
||||
|
||||
def upload_books(self, files, names, on_card=None, end_session=True,
|
||||
metadata=None):
|
||||
|
||||
path = self._sanity_check(on_card, files)
|
||||
|
||||
paths = []
|
||||
@ -101,13 +159,14 @@ class USBMS(CLI, Device):
|
||||
|
||||
for i, infile in enumerate(files):
|
||||
mdata, fname = metadata.next(), names.next()
|
||||
filepath = self.create_upload_path(path, mdata, fname)
|
||||
|
||||
filepath = self.normalize_path(self.create_upload_path(path, mdata, fname))
|
||||
paths.append(filepath)
|
||||
|
||||
if not hasattr(infile, 'read'):
|
||||
infile = self.normalize_path(infile)
|
||||
self.put_file(infile, filepath, replace_file=True)
|
||||
try:
|
||||
self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata)
|
||||
self.upload_cover(os.path.dirname(filepath),
|
||||
os.path.splitext(os.path.basename(filepath))[0], mdata)
|
||||
except: # Failure to upload cover is not catastrophic
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
@ -115,7 +174,6 @@ class USBMS(CLI, Device):
|
||||
self.report_progress((i+1) / float(len(files)), _('Transferring books to device...'))
|
||||
|
||||
self.report_progress(1.0, _('Transferring books to device...'))
|
||||
|
||||
return zip(paths, cycle([on_card]))
|
||||
|
||||
def upload_cover(self, path, filename, metadata):
|
||||
@ -128,20 +186,43 @@ class USBMS(CLI, Device):
|
||||
pass
|
||||
|
||||
def add_books_to_metadata(self, locations, metadata, booklists):
|
||||
metadata = iter(metadata)
|
||||
for i, location in enumerate(locations):
|
||||
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
|
||||
path = location[0]
|
||||
info = metadata.next()
|
||||
blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0
|
||||
|
||||
book = self.book_from_path(path)
|
||||
|
||||
if not book in booklists[blist]:
|
||||
booklists[blist].append(book)
|
||||
# Extract the correct prefix from the pathname. To do this correctly,
|
||||
# we must ensure that both the prefix and the path are normalized
|
||||
# so that the comparison will work. Book's __init__ will fix up
|
||||
# lpath, so we don't need to worry about that here.
|
||||
path = self.normalize_path(location[0])
|
||||
if self._main_prefix:
|
||||
prefix = self._main_prefix if \
|
||||
path.startswith(self.normalize_path(self._main_prefix)) else None
|
||||
if not prefix and self._card_a_prefix:
|
||||
prefix = self._card_a_prefix if \
|
||||
path.startswith(self.normalize_path(self._card_a_prefix)) else None
|
||||
if not prefix and self._card_b_prefix:
|
||||
prefix = self._card_b_prefix if \
|
||||
path.startswith(self.normalize_path(self._card_b_prefix)) else None
|
||||
if prefix is None:
|
||||
prints('in add_books_to_metadata. Prefix is None!', path,
|
||||
self._main_prefix)
|
||||
continue
|
||||
lpath = path.partition(prefix)[2]
|
||||
if lpath.startswith('/') or lpath.startswith('\\'):
|
||||
lpath = lpath[1:]
|
||||
book = self.book_class(prefix, lpath, other=info)
|
||||
if book.size is None:
|
||||
book.size = os.stat(self.normalize_path(path)).st_size
|
||||
booklists[blist].add_book(book, replace_metadata=True)
|
||||
self.report_progress(1.0, _('Adding books to device metadata listing...'))
|
||||
|
||||
def delete_books(self, paths, end_session=True):
|
||||
for i, path in enumerate(paths):
|
||||
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
|
||||
path = self.normalize_path(path)
|
||||
if os.path.exists(path):
|
||||
# Delete the ebook
|
||||
os.unlink(path)
|
||||
@ -166,15 +247,79 @@ class USBMS(CLI, Device):
|
||||
for bl in booklists:
|
||||
for book in bl:
|
||||
if path.endswith(book.path):
|
||||
bl.remove(book)
|
||||
bl.remove_book(book)
|
||||
self.report_progress(1.0, _('Removing books from device metadata listing...'))
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
# There is no meta data on the device to update. The device is treated
|
||||
# as a mass storage device and does not use a meta data xml file like
|
||||
# the Sony Readers.
|
||||
if not os.path.exists(self.normalize_path(self._main_prefix)):
|
||||
os.makedirs(self.normalize_path(self._main_prefix))
|
||||
|
||||
def write_prefix(prefix, listid):
|
||||
if prefix is not None and isinstance(booklists[listid], self.booklist_class):
|
||||
if not os.path.exists(prefix):
|
||||
os.makedirs(self.normalize_path(prefix))
|
||||
js = [item.to_json() for item in booklists[listid] if
|
||||
hasattr(item, 'to_json')]
|
||||
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
|
||||
json.dump(js, f, indent=2, encoding='utf-8')
|
||||
write_prefix(self._main_prefix, 0)
|
||||
write_prefix(self._card_a_prefix, 1)
|
||||
write_prefix(self._card_b_prefix, 2)
|
||||
|
||||
self.report_progress(1.0, _('Sending metadata to device...'))
|
||||
|
||||
@classmethod
|
||||
def path_to_unicode(cls, path):
|
||||
if isbytestring(path):
|
||||
path = path.decode(filesystem_encoding)
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def normalize_path(cls, path):
|
||||
'Return path with platform native path separators'
|
||||
if path is None:
|
||||
return None
|
||||
if os.sep == '\\':
|
||||
path = path.replace('/', '\\')
|
||||
else:
|
||||
path = path.replace('\\', '/')
|
||||
return cls.path_to_unicode(path)
|
||||
|
||||
@classmethod
|
||||
def parse_metadata_cache(cls, bl, prefix, name):
|
||||
# bl = cls.booklist_class()
|
||||
js = []
|
||||
need_sync = False
|
||||
cache_file = cls.normalize_path(os.path.join(prefix, name))
|
||||
if os.access(cache_file, os.R_OK):
|
||||
try:
|
||||
with open(cache_file, 'rb') as f:
|
||||
js = json.load(f, encoding='utf-8')
|
||||
for item in js:
|
||||
book = cls.book_class(prefix, item.get('lpath', None))
|
||||
for key in item.keys():
|
||||
setattr(book, key, item[key])
|
||||
bl.append(book)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
bl = []
|
||||
need_sync = True
|
||||
else:
|
||||
need_sync = True
|
||||
return need_sync
|
||||
|
||||
@classmethod
|
||||
def update_metadata_item(cls, book):
|
||||
changed = False
|
||||
size = os.stat(cls.normalize_path(book.path)).st_size
|
||||
if size != book.size:
|
||||
changed = True
|
||||
mi = cls.metadata_from_path(book.path)
|
||||
book.smart_update(mi)
|
||||
book.size = size
|
||||
return changed
|
||||
|
||||
@classmethod
|
||||
def metadata_from_path(cls, path):
|
||||
return cls.metadata_from_formats([path])
|
||||
@ -187,23 +332,19 @@ class USBMS(CLI, Device):
|
||||
return metadata_from_formats(fmts)
|
||||
|
||||
@classmethod
|
||||
def book_from_path(cls, path):
|
||||
from calibre.ebooks.metadata.meta import path_to_ext
|
||||
def book_from_path(cls, prefix, path):
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
mime = mime_type_ext(path_to_ext(path))
|
||||
|
||||
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
|
||||
mi = cls.metadata_from_path(path)
|
||||
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, path)))
|
||||
else:
|
||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||
mi = metadata_from_filename(os.path.basename(path),
|
||||
mi = metadata_from_filename(cls.normalize_path(os.path.basename(path)),
|
||||
re.compile(r'^(?P<title>[ \S]+?)[ _]-[ _](?P<author>[ \S]+?)_+\d+'))
|
||||
|
||||
if mi is None:
|
||||
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0],
|
||||
[_('Unknown')])
|
||||
|
||||
authors = authors_to_string(mi.authors)
|
||||
|
||||
book = Book(path, mi.title, authors, mime)
|
||||
size = os.stat(cls.normalize_path(os.path.join(prefix, path))).st_size
|
||||
book = cls.book_class(prefix, path, other=mi, size=size)
|
||||
return book
|
||||
|
@ -14,8 +14,14 @@ XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>')
|
||||
SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
XLINK_NS = 'http://www.w3.org/1999/xlink'
|
||||
|
||||
convert_entities = functools.partial(entity_to_unicode, exceptions=['quot',
|
||||
'apos', 'lt', 'gt', 'amp', '#60', '#62'])
|
||||
convert_entities = functools.partial(entity_to_unicode,
|
||||
result_exceptions = {
|
||||
u'<' : '<',
|
||||
u'>' : '>',
|
||||
u"'" : ''',
|
||||
u'"' : '"',
|
||||
u'&' : '&',
|
||||
})
|
||||
_span_pat = re.compile('<span.*?</span>', re.DOTALL|re.IGNORECASE)
|
||||
|
||||
LIGATURES = {
|
||||
|
@ -416,9 +416,9 @@ class HTMLInput(InputFormatPlugin):
|
||||
link = unquote(link).replace('/', os.sep)
|
||||
if not link.strip():
|
||||
return link_
|
||||
try:
|
||||
if base and not os.path.isabs(link):
|
||||
link = os.path.join(base, link)
|
||||
try:
|
||||
link = os.path.abspath(link)
|
||||
except:
|
||||
return link_
|
||||
|
@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import struct, array, zlib, cStringIO, collections, re
|
||||
|
||||
from calibre.ebooks.lrf import LRFParseError, PRS500_PROFILE
|
||||
from calibre import entity_to_unicode
|
||||
from calibre import entity_to_unicode, prepare_string_for_xml
|
||||
from calibre.ebooks.lrf.tags import Tag
|
||||
|
||||
ruby_tags = {
|
||||
@ -870,7 +870,7 @@ class Text(LRFStream):
|
||||
open_containers = collections.deque()
|
||||
for c in self.content:
|
||||
if isinstance(c, basestring):
|
||||
s += c
|
||||
s += prepare_string_for_xml(c)
|
||||
elif c is None:
|
||||
if open_containers:
|
||||
p = open_containers.pop()
|
||||
|
@ -10,7 +10,7 @@ import os, mimetypes, sys, re
|
||||
from urllib import unquote, quote
|
||||
from urlparse import urlparse
|
||||
|
||||
from calibre import relpath
|
||||
from calibre import relpath, prints
|
||||
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.date import isoformat
|
||||
@ -253,6 +253,15 @@ class MetaInformation(object):
|
||||
):
|
||||
setattr(self, x, getattr(mi, x, None))
|
||||
|
||||
def print_all_attributes(self):
|
||||
for x in ('author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
||||
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
||||
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
||||
'rights', 'publication_type', 'uuid'
|
||||
):
|
||||
prints(x, getattr(self, x, 'None'))
|
||||
|
||||
def smart_update(self, mi):
|
||||
'''
|
||||
Merge the information in C{mi} into self. In case of conflicts, the information
|
||||
@ -269,7 +278,7 @@ class MetaInformation(object):
|
||||
'isbn', 'application_id', 'manifest', 'spine', 'toc',
|
||||
'cover', 'language', 'guide', 'book_producer',
|
||||
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
|
||||
'publication_type', 'uuid',):
|
||||
'publication_type', 'uuid'):
|
||||
if hasattr(mi, attr):
|
||||
val = getattr(mi, attr)
|
||||
if val is not None:
|
||||
|
99
src/calibre/ebooks/metadata/book/__init__.py
Normal file
@ -0,0 +1,99 @@
|
||||
#!/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'
|
||||
|
||||
'''
|
||||
All fields must have a NULL value represented as None for simple types,
|
||||
an empty list/dictionary for complex types and (None, None) for cover_data
|
||||
'''
|
||||
|
||||
SOCIAL_METADATA_FIELDS = frozenset([
|
||||
'tags', # Ordered list
|
||||
# A floating point number between 0 and 10
|
||||
'rating',
|
||||
# A simple HTML enabled string
|
||||
'comments',
|
||||
# A simple string
|
||||
'series',
|
||||
# A floating point number
|
||||
'series_index',
|
||||
# Of the form { scheme1:value1, scheme2:value2}
|
||||
# For example: {'isbn':'123456789', 'doi':'xxxx', ... }
|
||||
'classifiers',
|
||||
'isbn', # Pseudo field for convenience, should get/set isbn classifier
|
||||
|
||||
])
|
||||
|
||||
PUBLICATION_METADATA_FIELDS = frozenset([
|
||||
# title must never be None. Should be _('Unknown')
|
||||
'title',
|
||||
# Pseudo field that can be set, but if not set is auto generated
|
||||
# from title and languages
|
||||
'title_sort',
|
||||
# Ordered list of authors. Must never be None, can be [_('Unknown')]
|
||||
'authors',
|
||||
# Pseudo field that can be set, but if not set is auto generated
|
||||
# from authors and languages
|
||||
'author_sort',
|
||||
'book_producer',
|
||||
# Dates and times must be timezone aware
|
||||
'timestamp',
|
||||
'pubdate',
|
||||
'rights',
|
||||
# So far only known publication type is periodical:calibre
|
||||
# If None, means book
|
||||
'publication_type',
|
||||
# A UUID usually of type 4
|
||||
'uuid',
|
||||
'languages', # ordered list
|
||||
# Simple string, no special semantics
|
||||
'publisher',
|
||||
# Absolute path to image file encoded in filesystem_encoding
|
||||
'cover',
|
||||
# Of the form (format, data) where format is, for e.g. 'jpeg', 'png', 'gif'...
|
||||
'cover_data',
|
||||
# Either thumbnail data, or an object with the attribute
|
||||
# image_path which is the path to an image file, encoded
|
||||
# in filesystem_encoding
|
||||
'thumbnail',
|
||||
])
|
||||
|
||||
BOOK_STRUCTURE_FIELDS = frozenset([
|
||||
# These are used by code, Null values are None.
|
||||
'toc', 'spine', 'guide', 'manifest',
|
||||
])
|
||||
|
||||
USER_METADATA_FIELDS = frozenset([
|
||||
# A dict of a form to be specified
|
||||
'user_metadata',
|
||||
])
|
||||
|
||||
DEVICE_METADATA_FIELDS = frozenset([
|
||||
# Ordered list of strings
|
||||
'device_collections',
|
||||
'lpath', # Unicode, / separated
|
||||
# In bytes
|
||||
'size',
|
||||
# Mimetype of the book file being represented
|
||||
'mime',
|
||||
])
|
||||
|
||||
CALIBRE_METADATA_FIELDS = frozenset([
|
||||
# An application id
|
||||
# Semantics to be defined. Is it a db key? a db name + key? A uuid?
|
||||
'application_id',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||
USER_METADATA_FIELDS).union(
|
||||
PUBLICATION_METADATA_FIELDS).union(
|
||||
CALIBRE_METADATA_FIELDS).union(
|
||||
frozenset(['lpath'])) # I don't think we need device_collections
|
||||
|
||||
# Serialization of covers/thumbnails will have to be handled carefully, maybe
|
||||
# as an option to the serializer class
|
120
src/calibre/ebooks/metadata/book/base.py
Normal file
@ -0,0 +1,120 @@
|
||||
#!/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 copy
|
||||
|
||||
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
|
||||
|
||||
NULL_VALUES = {
|
||||
'user_metadata': {},
|
||||
'cover_data' : (None, None),
|
||||
'tags' : [],
|
||||
'classifiers' : {},
|
||||
'languages' : [],
|
||||
'device_collections': [],
|
||||
'authors' : [_('Unknown')],
|
||||
'title' : _('Unknown'),
|
||||
}
|
||||
|
||||
class Metadata(object):
|
||||
|
||||
'''
|
||||
This class must expose a superset of the API of MetaInformation in terms
|
||||
of attribute access and methods. Only the __init__ method is different.
|
||||
MetaInformation will simply become a function that creates and fills in
|
||||
the attributes of this class.
|
||||
|
||||
Please keep the method based API of this class to a minimum. Every method
|
||||
becomes a reserved field name.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
object.__setattr__(self, '_data', copy.deepcopy(NULL_VALUES))
|
||||
|
||||
def __getattribute__(self, field):
|
||||
_data = object.__getattribute__(self, '_data')
|
||||
if field in RESERVED_METADATA_FIELDS:
|
||||
return _data.get(field, None)
|
||||
try:
|
||||
return object.__getattribute__(self, field)
|
||||
except AttributeError:
|
||||
pass
|
||||
if field in _data['user_metadata'].iterkeys():
|
||||
# TODO: getting user metadata values
|
||||
pass
|
||||
raise AttributeError(
|
||||
'Metadata object has no attribute named: '+ repr(field))
|
||||
|
||||
|
||||
def __setattr__(self, field, val):
|
||||
_data = object.__getattribute__(self, '_data')
|
||||
if field in RESERVED_METADATA_FIELDS:
|
||||
if field != 'user_metadata':
|
||||
if not val:
|
||||
val = NULL_VALUES[field]
|
||||
_data[field] = val
|
||||
else:
|
||||
raise AttributeError('You cannot set user_metadata directly.')
|
||||
elif field in _data['user_metadata'].iterkeys():
|
||||
# TODO: Setting custom column values
|
||||
pass
|
||||
else:
|
||||
# You are allowed to stick arbitrary attributes onto this object as
|
||||
# long as they dont conflict with global or user metadata names
|
||||
# Don't abuse this privilege
|
||||
self.__dict__[field] = val
|
||||
|
||||
@property
|
||||
def user_metadata_names(self):
|
||||
'The set of user metadata names this object knows about'
|
||||
_data = object.__getattribute__(self, '_data')
|
||||
return frozenset(_data['user_metadata'].iterkeys())
|
||||
|
||||
# Old MetaInformation API {{{
|
||||
def copy(self):
|
||||
pass
|
||||
|
||||
def print_all_attributes(self):
|
||||
pass
|
||||
|
||||
def smart_update(self, other):
|
||||
pass
|
||||
|
||||
def format_series_index(self):
|
||||
pass
|
||||
|
||||
def authors_from_string(self, raw):
|
||||
pass
|
||||
|
||||
def format_authors(self):
|
||||
pass
|
||||
|
||||
def format_tags(self):
|
||||
pass
|
||||
|
||||
def format_rating(self):
|
||||
return unicode(self.rating)
|
||||
|
||||
def __unicode__(self):
|
||||
pass
|
||||
|
||||
def to_html(self):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__().encode('utf-8')
|
||||
|
||||
def __nonzero__(self):
|
||||
return True
|
||||
|
||||
# }}}
|
||||
|
||||
# We don't need reserved field names for this object any more. Lets just use a
|
||||
# protocol like the last char of a user field label should be _ when using this
|
||||
# object
|
||||
# So mi.tags returns the builtin tags and mi.tags_ returns the user tags
|
||||
|
@ -9,7 +9,6 @@ from threading import Thread
|
||||
from calibre import prints
|
||||
from calibre.utils.config import OptionParser
|
||||
from calibre.utils.logging import default_log
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.customize import Plugin
|
||||
|
||||
metadata_config = None
|
||||
@ -53,7 +52,7 @@ class MetadataSource(Plugin):
|
||||
if self.results:
|
||||
c = self.config_store().get(self.name, {})
|
||||
res = self.results
|
||||
if isinstance(res, MetaInformation):
|
||||
if hasattr(res, 'authors'):
|
||||
res = [res]
|
||||
for mi in res:
|
||||
if not c.get('rating', True):
|
||||
|
@ -11,7 +11,7 @@ import re
|
||||
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
|
||||
from calibre import entity_to_unicode
|
||||
|
||||
def get_metadata(stream):
|
||||
src = stream.read()
|
||||
@ -43,6 +43,10 @@ def get_metadata_(src, encoding=None):
|
||||
if match:
|
||||
author = match.group(2).replace(',', ';')
|
||||
|
||||
ent_pat = re.compile(r'&(\S+)?;')
|
||||
title = ent_pat.sub(entity_to_unicode, title)
|
||||
if author:
|
||||
author = ent_pat.sub(entity_to_unicode, author)
|
||||
mi = MetaInformation(title, [author] if author else None)
|
||||
|
||||
# Publisher
|
||||
|
@ -5,9 +5,9 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, re, collections
|
||||
|
||||
from calibre.utils.config import prefs
|
||||
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.customize.ui import get_file_type_metadata, set_file_type_metadata
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||
|
||||
@ -131,6 +131,8 @@ def set_metadata(stream, mi, stream_type='lrf'):
|
||||
|
||||
|
||||
def metadata_from_filename(name, pat=None):
|
||||
if isbytestring(name):
|
||||
name = name.decode(filesystem_encoding, 'replace')
|
||||
name = name.rpartition('.')[0]
|
||||
mi = MetaInformation(None, None)
|
||||
if pat is None:
|
||||
|
0
src/calibre/ebooks/metadata/odt.py
Executable file → Normal file
@ -1,538 +0,0 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''Read/Write metadata from Open Packaging Format (.opf) files.'''
|
||||
|
||||
import re, os
|
||||
import uuid
|
||||
from urllib import unquote, quote
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
|
||||
from calibre.ebooks.lrf import entity_to_unicode
|
||||
from calibre.ebooks.metadata import Resource, ResourceCollection
|
||||
from calibre.ebooks.metadata.toc import TOC
|
||||
|
||||
class OPFSoup(BeautifulStoneSoup):
|
||||
|
||||
def __init__(self, raw):
|
||||
BeautifulStoneSoup.__init__(self, raw,
|
||||
convertEntities=BeautifulSoup.HTML_ENTITIES,
|
||||
selfClosingTags=['item', 'itemref', 'reference'])
|
||||
|
||||
class ManifestItem(Resource):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_manifest_item(item, basedir):
|
||||
if item.has_key('href'):
|
||||
href = item['href']
|
||||
if unquote(href) == href:
|
||||
try:
|
||||
href = quote(href)
|
||||
except KeyError:
|
||||
pass
|
||||
res = ManifestItem(href, basedir=basedir, is_path=False)
|
||||
mt = item.get('media-type', '').strip()
|
||||
if mt:
|
||||
res.mime_type = mt
|
||||
return res
|
||||
|
||||
@dynamic_property
|
||||
def media_type(self):
|
||||
def fget(self):
|
||||
return self.mime_type
|
||||
def fset(self, val):
|
||||
self.mime_type = val
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode(self)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index == 0:
|
||||
return self.href()
|
||||
if index == 1:
|
||||
return self.media_type
|
||||
raise IndexError('%d out of bounds.'%index)
|
||||
|
||||
|
||||
class Manifest(ResourceCollection):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_manifest_element(manifest, dir):
|
||||
m = Manifest()
|
||||
for item in manifest.findAll(re.compile('item')):
|
||||
try:
|
||||
m.append(ManifestItem.from_opf_manifest_item(item, dir))
|
||||
id = item.get('id', '')
|
||||
if not id:
|
||||
id = 'id%d'%m.next_id
|
||||
m[-1].id = id
|
||||
m.next_id += 1
|
||||
except ValueError:
|
||||
continue
|
||||
return m
|
||||
|
||||
@staticmethod
|
||||
def from_paths(entries):
|
||||
'''
|
||||
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
|
||||
'''
|
||||
m = Manifest()
|
||||
for path, mt in entries:
|
||||
mi = ManifestItem(path, is_path=True)
|
||||
if mt:
|
||||
mi.mime_type = mt
|
||||
mi.id = 'id%d'%m.next_id
|
||||
m.next_id += 1
|
||||
m.append(mi)
|
||||
return m
|
||||
|
||||
def __init__(self):
|
||||
ResourceCollection.__init__(self)
|
||||
self.next_id = 1
|
||||
|
||||
|
||||
def item(self, id):
|
||||
for i in self:
|
||||
if i.id == id:
|
||||
return i
|
||||
|
||||
def id_for_path(self, path):
|
||||
path = os.path.normpath(os.path.abspath(path))
|
||||
for i in self:
|
||||
if i.path and os.path.normpath(i.path) == path:
|
||||
return i.id
|
||||
|
||||
def path_for_id(self, id):
|
||||
for i in self:
|
||||
if i.id == id:
|
||||
return i.path
|
||||
|
||||
class Spine(ResourceCollection):
|
||||
|
||||
class Item(Resource):
|
||||
|
||||
def __init__(self, idfunc, *args, **kwargs):
|
||||
Resource.__init__(self, *args, **kwargs)
|
||||
self.is_linear = True
|
||||
self.id = idfunc(self.path)
|
||||
|
||||
@staticmethod
|
||||
def from_opf_spine_element(spine, manifest):
|
||||
s = Spine(manifest)
|
||||
for itemref in spine.findAll(re.compile('itemref')):
|
||||
if itemref.has_key('idref'):
|
||||
r = Spine.Item(s.manifest.id_for_path,
|
||||
s.manifest.path_for_id(itemref['idref']), is_path=True)
|
||||
r.is_linear = itemref.get('linear', 'yes') == 'yes'
|
||||
s.append(r)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def from_paths(paths, manifest):
|
||||
s = Spine(manifest)
|
||||
for path in paths:
|
||||
try:
|
||||
s.append(Spine.Item(s.manifest.id_for_path, path, is_path=True))
|
||||
except:
|
||||
continue
|
||||
return s
|
||||
|
||||
|
||||
|
||||
def __init__(self, manifest):
|
||||
ResourceCollection.__init__(self)
|
||||
self.manifest = manifest
|
||||
|
||||
|
||||
def linear_items(self):
|
||||
for r in self:
|
||||
if r.is_linear:
|
||||
yield r.path
|
||||
|
||||
def nonlinear_items(self):
|
||||
for r in self:
|
||||
if not r.is_linear:
|
||||
yield r.path
|
||||
|
||||
def items(self):
|
||||
for i in self:
|
||||
yield i.path
|
||||
|
||||
|
||||
class Guide(ResourceCollection):
|
||||
|
||||
class Reference(Resource):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_resource_item(ref, basedir):
|
||||
title, href, type = ref.get('title', ''), ref['href'], ref['type']
|
||||
res = Guide.Reference(href, basedir, is_path=False)
|
||||
res.title = title
|
||||
res.type = type
|
||||
return res
|
||||
|
||||
def __repr__(self):
|
||||
ans = '<reference type="%s" href="%s" '%(self.type, self.href())
|
||||
if self.title:
|
||||
ans += 'title="%s" '%self.title
|
||||
return ans + '/>'
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_opf_guide(guide_elem, base_dir=os.getcwdu()):
|
||||
coll = Guide()
|
||||
for ref in guide_elem.findAll('reference'):
|
||||
try:
|
||||
ref = Guide.Reference.from_opf_resource_item(ref, base_dir)
|
||||
coll.append(ref)
|
||||
except:
|
||||
continue
|
||||
return coll
|
||||
|
||||
def set_cover(self, path):
|
||||
map(self.remove, [i for i in self if 'cover' in i.type.lower()])
|
||||
for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
|
||||
self.append(Guide.Reference(path, is_path=True))
|
||||
self[-1].type = type
|
||||
self[-1].title = ''
|
||||
|
||||
|
||||
class standard_field(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
return getattr(obj, 'get_'+self.name)()
|
||||
|
||||
|
||||
class OPF(MetaInformation):
|
||||
|
||||
MIMETYPE = 'application/oebps-package+xml'
|
||||
ENTITY_PATTERN = re.compile(r'&(\S+?);')
|
||||
|
||||
uid = standard_field('uid')
|
||||
application_id = standard_field('application_id')
|
||||
title = standard_field('title')
|
||||
authors = standard_field('authors')
|
||||
language = standard_field('language')
|
||||
title_sort = standard_field('title_sort')
|
||||
author_sort = standard_field('author_sort')
|
||||
comments = standard_field('comments')
|
||||
category = standard_field('category')
|
||||
publisher = standard_field('publisher')
|
||||
isbn = standard_field('isbn')
|
||||
cover = standard_field('cover')
|
||||
series = standard_field('series')
|
||||
series_index = standard_field('series_index')
|
||||
rating = standard_field('rating')
|
||||
tags = standard_field('tags')
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError('Abstract base class')
|
||||
|
||||
@dynamic_property
|
||||
def package(self):
|
||||
def fget(self):
|
||||
return self.soup.find(re.compile('package'))
|
||||
return property(fget=fget)
|
||||
|
||||
@dynamic_property
|
||||
def metadata(self):
|
||||
def fget(self):
|
||||
return self.package.find(re.compile('metadata'))
|
||||
return property(fget=fget)
|
||||
|
||||
|
||||
def get_title(self):
|
||||
title = self.metadata.find('dc:title')
|
||||
if title and title.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
|
||||
return self.default_title.strip()
|
||||
|
||||
def get_authors(self):
|
||||
creators = self.metadata.findAll('dc:creator')
|
||||
for elem in creators:
|
||||
role = elem.get('role')
|
||||
if not role:
|
||||
role = elem.get('opf:role')
|
||||
if not role:
|
||||
role = 'aut'
|
||||
if role == 'aut' and elem.string:
|
||||
raw = self.ENTITY_PATTERN.sub(entity_to_unicode, elem.string)
|
||||
return string_to_authors(raw)
|
||||
return []
|
||||
|
||||
def get_author_sort(self):
|
||||
creators = self.metadata.findAll('dc:creator')
|
||||
for elem in creators:
|
||||
role = elem.get('role')
|
||||
if not role:
|
||||
role = elem.get('opf:role')
|
||||
if role == 'aut':
|
||||
fa = elem.get('file-as')
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None
|
||||
return None
|
||||
|
||||
def get_title_sort(self):
|
||||
title = self.package.find('dc:title')
|
||||
if title:
|
||||
if title.has_key('file-as'):
|
||||
return title['file-as'].strip()
|
||||
return None
|
||||
|
||||
def get_comments(self):
|
||||
comments = self.soup.find('dc:description')
|
||||
if comments and comments.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip()
|
||||
return None
|
||||
|
||||
def get_uid(self):
|
||||
package = self.package
|
||||
if package.has_key('unique-identifier'):
|
||||
return package['unique-identifier']
|
||||
|
||||
def get_category(self):
|
||||
category = self.soup.find('dc:type')
|
||||
if category and category.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
|
||||
return None
|
||||
|
||||
def get_publisher(self):
|
||||
publisher = self.soup.find('dc:publisher')
|
||||
if publisher and publisher.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
|
||||
return None
|
||||
|
||||
def get_isbn(self):
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme')
|
||||
if not scheme:
|
||||
scheme = item.get('opf:scheme')
|
||||
if scheme is not None and scheme.lower() == 'isbn' and item.string:
|
||||
return str(item.string).strip()
|
||||
return None
|
||||
|
||||
def get_language(self):
|
||||
item = self.metadata.find('dc:language')
|
||||
if not item:
|
||||
return _('Unknown')
|
||||
return ''.join(item.findAll(text=True)).strip()
|
||||
|
||||
def get_application_id(self):
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme', None)
|
||||
if scheme is None:
|
||||
scheme = item.get('opf:scheme', None)
|
||||
if scheme in ['libprs500', 'calibre']:
|
||||
return str(item.string).strip()
|
||||
return None
|
||||
|
||||
def get_cover(self):
|
||||
guide = getattr(self, 'guide', [])
|
||||
if not guide:
|
||||
guide = []
|
||||
references = [ref for ref in guide if 'cover' in ref.type.lower()]
|
||||
for candidate in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
|
||||
matches = [r for r in references if r.type.lower() == candidate and r.path]
|
||||
if matches:
|
||||
return matches[0].path
|
||||
|
||||
def possible_cover_prefixes(self):
|
||||
isbn, ans = [], []
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme')
|
||||
if not scheme:
|
||||
scheme = item.get('opf:scheme')
|
||||
isbn.append((scheme, item.string))
|
||||
for item in isbn:
|
||||
ans.append(item[1].replace('-', ''))
|
||||
return ans
|
||||
|
||||
def get_series(self):
|
||||
s = self.metadata.find('series')
|
||||
if s is not None:
|
||||
return str(s.string).strip()
|
||||
return None
|
||||
|
||||
def get_series_index(self):
|
||||
s = self.metadata.find('series-index')
|
||||
if s and s.string:
|
||||
try:
|
||||
return float(str(s.string).strip())
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_rating(self):
|
||||
s = self.metadata.find('rating')
|
||||
if s and s.string:
|
||||
try:
|
||||
return int(str(s.string).strip())
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_tags(self):
|
||||
ans = []
|
||||
subs = self.soup.findAll('dc:subject')
|
||||
for sub in subs:
|
||||
val = sub.string
|
||||
if val:
|
||||
ans.append(val)
|
||||
return [unicode(a).strip() for a in ans]
|
||||
|
||||
|
||||
class OPFReader(OPF):
|
||||
|
||||
def __init__(self, stream, dir=os.getcwdu()):
|
||||
manage = False
|
||||
if not hasattr(stream, 'read'):
|
||||
manage = True
|
||||
dir = os.path.dirname(stream)
|
||||
stream = open(stream, 'rb')
|
||||
self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown'
|
||||
if hasattr(stream, 'seek'):
|
||||
stream.seek(0)
|
||||
self.soup = OPFSoup(stream.read())
|
||||
if manage:
|
||||
stream.close()
|
||||
self.manifest = Manifest()
|
||||
m = self.soup.find(re.compile('manifest'))
|
||||
if m is not None:
|
||||
self.manifest = Manifest.from_opf_manifest_element(m, dir)
|
||||
self.spine = None
|
||||
spine = self.soup.find(re.compile('spine'))
|
||||
if spine is not None:
|
||||
self.spine = Spine.from_opf_spine_element(spine, self.manifest)
|
||||
|
||||
self.toc = TOC(base_path=dir)
|
||||
self.toc.read_from_opf(self)
|
||||
guide = self.soup.find(re.compile('guide'))
|
||||
if guide is not None:
|
||||
self.guide = Guide.from_opf_guide(guide, dir)
|
||||
self.base_dir = dir
|
||||
self.cover_data = (None, None)
|
||||
|
||||
|
||||
class OPFCreator(MetaInformation):
|
||||
|
||||
def __init__(self, base_path, *args, **kwargs):
|
||||
'''
|
||||
Initialize.
|
||||
@param base_path: An absolute path to the directory in which this OPF file
|
||||
will eventually be. This is used by the L{create_manifest} method
|
||||
to convert paths to files into relative paths.
|
||||
'''
|
||||
MetaInformation.__init__(self, *args, **kwargs)
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
if self.application_id is None:
|
||||
self.application_id = str(uuid.uuid4())
|
||||
if not isinstance(self.toc, TOC):
|
||||
self.toc = None
|
||||
if not self.authors:
|
||||
self.authors = [_('Unknown')]
|
||||
if self.guide is None:
|
||||
self.guide = Guide()
|
||||
if self.cover:
|
||||
self.guide.set_cover(self.cover)
|
||||
|
||||
|
||||
def create_manifest(self, entries):
|
||||
'''
|
||||
Create <manifest>
|
||||
|
||||
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
|
||||
'''
|
||||
entries = map(lambda x: x if os.path.isabs(x[0]) else
|
||||
(os.path.abspath(os.path.join(self.base_path, x[0])), x[1]),
|
||||
entries)
|
||||
self.manifest = Manifest.from_paths(entries)
|
||||
self.manifest.set_basedir(self.base_path)
|
||||
|
||||
def create_manifest_from_files_in(self, files_and_dirs):
|
||||
entries = []
|
||||
|
||||
def dodir(dir):
|
||||
for spec in os.walk(dir):
|
||||
root, files = spec[0], spec[-1]
|
||||
for name in files:
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isfile(path):
|
||||
entries.append((path, None))
|
||||
|
||||
for i in files_and_dirs:
|
||||
if os.path.isdir(i):
|
||||
dodir(i)
|
||||
else:
|
||||
entries.append((i, None))
|
||||
|
||||
self.create_manifest(entries)
|
||||
|
||||
def create_spine(self, entries):
|
||||
'''
|
||||
Create the <spine> element. Must first call :method:`create_manifest`.
|
||||
|
||||
`entries`: List of paths
|
||||
'''
|
||||
entries = map(lambda x: x if os.path.isabs(x) else
|
||||
os.path.abspath(os.path.join(self.base_path, x)), entries)
|
||||
self.spine = Spine.from_paths(entries, self.manifest)
|
||||
|
||||
def set_toc(self, toc):
|
||||
'''
|
||||
Set the toc. You must call :method:`create_spine` before calling this
|
||||
method.
|
||||
|
||||
:param toc: A :class:`TOC` object
|
||||
'''
|
||||
self.toc = toc
|
||||
|
||||
def create_guide(self, guide_element):
|
||||
self.guide = Guide.from_opf_guide(guide_element, self.base_path)
|
||||
self.guide.set_basedir(self.base_path)
|
||||
|
||||
def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
opf_template = open(P('templates/opf.xml'), 'rb').read()
|
||||
template = MarkupTemplate(opf_template)
|
||||
if self.manifest:
|
||||
self.manifest.set_basedir(self.base_path)
|
||||
if ncx_manifest_entry is not None:
|
||||
if not os.path.isabs(ncx_manifest_entry):
|
||||
ncx_manifest_entry = os.path.join(self.base_path, ncx_manifest_entry)
|
||||
remove = [i for i in self.manifest if i.id == 'ncx']
|
||||
for item in remove:
|
||||
self.manifest.remove(item)
|
||||
self.manifest.append(ManifestItem(ncx_manifest_entry, self.base_path))
|
||||
self.manifest[-1].id = 'ncx'
|
||||
self.manifest[-1].mime_type = 'application/x-dtbncx+xml'
|
||||
if not self.guide:
|
||||
self.guide = Guide()
|
||||
if self.cover:
|
||||
cover = self.cover
|
||||
if not os.path.isabs(cover):
|
||||
cover = os.path.abspath(os.path.join(self.base_path, cover))
|
||||
self.guide.set_cover(cover)
|
||||
self.guide.set_basedir(self.base_path)
|
||||
|
||||
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
|
||||
if not opf.startswith('<?xml '):
|
||||
opf = '<?xml version="1.0" encoding="UTF-8"?>\n'+opf
|
||||
opf_stream.write(opf)
|
||||
opf_stream.flush()
|
||||
toc = getattr(self, 'toc', None)
|
||||
if toc is not None and ncx_stream is not None:
|
||||
toc.render(ncx_stream, self.application_id)
|
||||
ncx_stream.flush()
|
||||
|
@ -451,8 +451,6 @@ class OPF(object):
|
||||
guide_path = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]')
|
||||
|
||||
title = MetadataField('title', formatter=lambda x: re.sub(r'\s+', ' ', x))
|
||||
title_sort = MetadataField('title_sort', formatter=lambda x:
|
||||
re.sub(r'\s+', ' ', x), is_dc=False)
|
||||
publisher = MetadataField('publisher')
|
||||
language = MetadataField('language')
|
||||
comments = MetadataField('description')
|
||||
@ -708,6 +706,30 @@ class OPF(object):
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@dynamic_property
|
||||
def title_sort(self):
|
||||
|
||||
def fget(self):
|
||||
matches = self.title_path(self.metadata)
|
||||
if matches:
|
||||
for match in matches:
|
||||
ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
|
||||
if not ans:
|
||||
ans = match.get('file-as', None)
|
||||
if ans:
|
||||
return ans
|
||||
|
||||
def fset(self, val):
|
||||
matches = self.title_path(self.metadata)
|
||||
if matches:
|
||||
for key in matches[0].attrib:
|
||||
if key.endswith('file-as'):
|
||||
matches[0].attrib.pop(key)
|
||||
matches[0].set('{%s}file-as'%self.NAMESPACES['opf'], unicode(val))
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def tags(self):
|
||||
|
||||
@ -981,11 +1003,8 @@ class OPFCreator(MetaInformation):
|
||||
|
||||
def render(self, opf_stream=sys.stdout, ncx_stream=None,
|
||||
ncx_manifest_entry=None, encoding=None):
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
opf_template = open(P('templates/opf.xml'), 'rb').read()
|
||||
if encoding is None:
|
||||
encoding = 'utf-8'
|
||||
template = MarkupTemplate(opf_template)
|
||||
toc = getattr(self, 'toc', None)
|
||||
if self.manifest:
|
||||
self.manifest.set_basedir(self.base_path)
|
||||
@ -1006,12 +1025,101 @@ class OPFCreator(MetaInformation):
|
||||
cover = os.path.abspath(os.path.join(self.base_path, cover))
|
||||
self.guide.set_cover(cover)
|
||||
self.guide.set_basedir(self.base_path)
|
||||
opf = template.generate(
|
||||
__appname__=__appname__, mi=self,
|
||||
__version__=__version__).render('xml', encoding=encoding)
|
||||
opf_stream.write('<?xml version="1.0" encoding="%s" ?>\n'
|
||||
%encoding.upper())
|
||||
opf_stream.write(opf)
|
||||
|
||||
# Actual rendering
|
||||
from lxml.builder import ElementMaker
|
||||
from calibre.ebooks.oeb.base import OPF2_NS, DC11_NS, CALIBRE_NS
|
||||
DNS = OPF2_NS+'___xx___'
|
||||
E = ElementMaker(namespace=DNS, nsmap={None:DNS})
|
||||
M = ElementMaker(namespace=DNS,
|
||||
nsmap={'dc':DC11_NS, 'calibre':CALIBRE_NS, 'opf':OPF2_NS})
|
||||
DC = ElementMaker(namespace=DC11_NS)
|
||||
|
||||
def DC_ELEM(tag, text, dc_attrs={}, opf_attrs={}):
|
||||
if text:
|
||||
elem = getattr(DC, tag)(text, **dc_attrs)
|
||||
else:
|
||||
elem = getattr(DC, tag)(**dc_attrs)
|
||||
for k, v in opf_attrs.items():
|
||||
elem.set('{%s}%s'%(OPF2_NS, k), v)
|
||||
return elem
|
||||
|
||||
def CAL_ELEM(name, content):
|
||||
return M.meta(name=name, content=content)
|
||||
|
||||
metadata = M.metadata()
|
||||
a = metadata.append
|
||||
role = {}
|
||||
if self.title_sort:
|
||||
role = {'file-as':self.title_sort}
|
||||
a(DC_ELEM('title', self.title if self.title else _('Unknown'),
|
||||
opf_attrs=role))
|
||||
for i, author in enumerate(self.authors):
|
||||
fa = {'role':'aut'}
|
||||
if i == 0 and self.author_sort:
|
||||
fa['file-as'] = self.author_sort
|
||||
a(DC_ELEM('creator', author, opf_attrs=fa))
|
||||
a(DC_ELEM('contributor', '%s (%s) [%s]'%(__appname__, __version__,
|
||||
'http://calibre-ebook.com'), opf_attrs={'role':'bkp',
|
||||
'file-as':__appname__}))
|
||||
a(DC_ELEM('identifier', str(self.application_id),
|
||||
opf_attrs={'scheme':__appname__},
|
||||
dc_attrs={'id':__appname__+'_id'}))
|
||||
if getattr(self, 'pubdate', None) is not None:
|
||||
a(DC_ELEM('date', self.pubdate.isoformat()))
|
||||
a(DC_ELEM('language', self.language if self.language else 'UND'))
|
||||
if self.comments:
|
||||
a(DC_ELEM('description', self.comments))
|
||||
if self.publisher:
|
||||
a(DC_ELEM('publisher', self.publisher))
|
||||
if self.isbn:
|
||||
a(DC_ELEM('identifier', self.isbn, opf_attrs={'scheme':'ISBN'}))
|
||||
if self.rights:
|
||||
a(DC_ELEM('rights', self.rights))
|
||||
if self.tags:
|
||||
for tag in self.tags:
|
||||
a(DC_ELEM('subject', tag))
|
||||
if self.series:
|
||||
a(CAL_ELEM('calibre:series', self.series))
|
||||
if self.series_index is not None:
|
||||
a(CAL_ELEM('calibre:series_index', self.format_series_index()))
|
||||
if self.rating is not None:
|
||||
a(CAL_ELEM('calibre:rating', str(self.rating)))
|
||||
if self.timestamp is not None:
|
||||
a(CAL_ELEM('calibre:timestamp', self.timestamp.isoformat()))
|
||||
if self.publication_type is not None:
|
||||
a(CAL_ELEM('calibre:publication_type', self.publication_type))
|
||||
manifest = E.manifest()
|
||||
if self.manifest is not None:
|
||||
for ref in self.manifest:
|
||||
item = E.item(id=str(ref.id), href=ref.href())
|
||||
item.set('media-type', ref.mime_type)
|
||||
manifest.append(item)
|
||||
spine = E.spine()
|
||||
if self.toc is not None:
|
||||
spine.set('toc', 'ncx')
|
||||
if self.spine is not None:
|
||||
for ref in self.spine:
|
||||
spine.append(E.itemref(idref=ref.id))
|
||||
guide = E.guide()
|
||||
if self.guide is not None:
|
||||
for ref in self.guide:
|
||||
item = E.reference(type=ref.type, href=ref.href())
|
||||
if ref.title:
|
||||
item.set('title', ref.title)
|
||||
guide.append(item)
|
||||
|
||||
root = E.package(
|
||||
metadata,
|
||||
manifest,
|
||||
spine,
|
||||
guide
|
||||
)
|
||||
root.set('unique-identifier', __appname__+'_id')
|
||||
raw = etree.tostring(root, pretty_print=True, xml_declaration=True,
|
||||
encoding=encoding)
|
||||
raw = raw.replace(DNS, OPF2_NS)
|
||||
opf_stream.write(raw)
|
||||
opf_stream.flush()
|
||||
if toc is not None and ncx_stream is not None:
|
||||
toc.render(ncx_stream, self.application_id)
|
||||
@ -1159,12 +1267,14 @@ class OPFTest(unittest.TestCase):
|
||||
<package version="2.0" xmlns="http://www.idpf.org/2007/opf" >
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||
<dc:title opf:file-as="Wow">A Cool & © ß Title</dc:title>
|
||||
<creator opf:role="aut" file-as="Monkey">Monkey Kitchen, Next</creator>
|
||||
<creator opf:role="aut" file-as="Monkey">Monkey Kitchen</creator>
|
||||
<creator opf:role="aut">Next</creator>
|
||||
<dc:subject>One</dc:subject><dc:subject>Two</dc:subject>
|
||||
<dc:identifier scheme="ISBN">123456789</dc:identifier>
|
||||
<x-metadata>
|
||||
<series>A one book series</series>
|
||||
</x-metadata>
|
||||
<meta name="calibre:series" content="A one book series" />
|
||||
<meta name="calibre:rating" content="4"/>
|
||||
<meta name="calibre:publication_type" content="test"/>
|
||||
<meta name="calibre:series_index" content="2.5" />
|
||||
</metadata>
|
||||
<manifest>
|
||||
<item id="1" href="a%20%7E%20b" media-type="text/txt" />
|
||||
@ -1184,7 +1294,9 @@ class OPFTest(unittest.TestCase):
|
||||
self.assertEqual(opf.tags, ['One', 'Two'])
|
||||
self.assertEqual(opf.isbn, '123456789')
|
||||
self.assertEqual(opf.series, 'A one book series')
|
||||
self.assertEqual(opf.series_index, 1)
|
||||
self.assertEqual(opf.series_index, 2.5)
|
||||
self.assertEqual(opf.rating, 4)
|
||||
self.assertEqual(opf.publication_type, 'test')
|
||||
self.assertEqual(list(opf.itermanifest())[0].get('href'), 'a ~ b')
|
||||
|
||||
def testWriting(self):
|
||||
@ -1214,3 +1326,6 @@ def suite():
|
||||
|
||||
def test():
|
||||
unittest.TextTestRunner(verbosity=2).run(suite())
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
@ -1,14 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os, glob, re
|
||||
from urlparse import urlparse
|
||||
from urllib import unquote
|
||||
from uuid import uuid4
|
||||
|
||||
from calibre import __appname__
|
||||
from lxml import etree
|
||||
from lxml.builder import ElementMaker
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
|
||||
NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
|
||||
CALIBRE_NS = "http://calibre.kovidgoyal.net/2009/metadata"
|
||||
NSMAP = {
|
||||
None: NCX_NS,
|
||||
'calibre':CALIBRE_NS
|
||||
}
|
||||
|
||||
|
||||
E = ElementMaker(namespace=NCX_NS, nsmap=NSMAP)
|
||||
|
||||
C = ElementMaker(namespace=CALIBRE_NS, nsmap=NSMAP)
|
||||
|
||||
class NCXSoup(BeautifulStoneSoup):
|
||||
|
||||
NESTABLE_TAGS = {'navpoint':[]}
|
||||
@ -208,10 +225,46 @@ class TOC(list):
|
||||
self.add_item(href, fragment, txt)
|
||||
|
||||
def render(self, stream, uid):
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
ncx_template = open(P('templates/ncx.xml'), 'rb').read()
|
||||
doctype = ('ncx', "-//NISO//DTD ncx 2005-1//EN", "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd")
|
||||
template = MarkupTemplate(ncx_template)
|
||||
raw = template.generate(uid=uid, toc=self, __appname__=__appname__)
|
||||
raw = raw.render(doctype=doctype)
|
||||
root = E.ncx(
|
||||
E.head(
|
||||
E.meta(name='dtb:uid', content=str(uid)),
|
||||
E.meta(name='dtb:depth', content=str(self.depth())),
|
||||
E.meta(name='dtb:generator', content='%s (%s)'%(__appname__,
|
||||
__version__)),
|
||||
E.meta(name='dtb:totalPageCount', content='0'),
|
||||
E.meta(name='dtb:maxPageNumber', content='0'),
|
||||
),
|
||||
E.docTitle(E.text('Table of Contents')),
|
||||
)
|
||||
navmap = E.navMap()
|
||||
root.append(navmap)
|
||||
root.set('{http://www.w3.org/XML/1998/namespace}lang', 'en')
|
||||
|
||||
def navpoint(parent, np):
|
||||
text = np.text
|
||||
if not text:
|
||||
text = ''
|
||||
elem = E.navPoint(
|
||||
E.navLabel(E.text(re.sub(r'\s+', ' ', text))),
|
||||
E.content(src=unicode(np.href)+(('#' + unicode(np.fragment))
|
||||
if np.fragment else '')),
|
||||
id=str(uuid4()),
|
||||
playOrder=str(np.play_order)
|
||||
)
|
||||
au = getattr(np, 'author', None)
|
||||
if au:
|
||||
au = re.sub(r'\s+', ' ', au)
|
||||
elem.append(C.meta(au, name='author'))
|
||||
desc = getattr(np, 'description', None)
|
||||
if desc:
|
||||
desc = re.sub(r'\s+', ' ', desc)
|
||||
elem.append(C.meta(desc, name='description'))
|
||||
parent.append(elem)
|
||||
for np2 in np:
|
||||
navpoint(elem, np2)
|
||||
|
||||
for np in self:
|
||||
navpoint(navmap, np)
|
||||
raw = etree.tostring(root, encoding='utf-8', xml_declaration=True,
|
||||
pretty_print=True)
|
||||
stream.write(raw)
|
||||
|
@ -35,6 +35,8 @@ def read_metadata_(task, tdir, notification=lambda x,y:x):
|
||||
if mi.cover_data:
|
||||
cdata = mi.cover_data[-1]
|
||||
mi.cover_data = None
|
||||
if not mi.application_id:
|
||||
mi.application_id = '__calibre_dummy__'
|
||||
with open(os.path.join(tdir, '%s.opf'%id), 'wb') as f:
|
||||
f.write(metadata_to_opf(mi))
|
||||
if cdata:
|
||||
|
@ -1334,7 +1334,7 @@ class MobiWriter(object):
|
||||
item = self._oeb.manifest.hrefs[href]
|
||||
try:
|
||||
data = rescale_image(item.data, self._imagemax)
|
||||
except IOError:
|
||||
except:
|
||||
self._oeb.logger.warn('Bad image file %r' % item.href)
|
||||
continue
|
||||
self._records.append(data)
|
||||
|
@ -787,7 +787,6 @@ class Manifest(object):
|
||||
data = self.oeb.decode(data)
|
||||
data = self.oeb.html_preprocessor(data)
|
||||
|
||||
|
||||
# Remove DOCTYPE declaration as it messes up parsing
|
||||
# In particular, it causes tostring to insert xmlns
|
||||
# declarations, which messes up the coercing logic
|
||||
|
@ -136,6 +136,8 @@ class CoverManager(object):
|
||||
href = g['cover'].href
|
||||
else:
|
||||
href = self.default_cover()
|
||||
if href is None:
|
||||
return
|
||||
width, height = self.inspect_cover(href)
|
||||
if width is None or height is None:
|
||||
self.log.warning('Failed to read cover dimensions')
|
||||
|
@ -201,6 +201,7 @@ class CSSFlattener(object):
|
||||
tag = barename(node.tag)
|
||||
style = stylizer.style(node)
|
||||
cssdict = style.cssdict()
|
||||
font_size = style['font-size']
|
||||
if 'align' in node.attrib:
|
||||
cssdict['text-align'] = node.attrib['align']
|
||||
del node.attrib['align']
|
||||
@ -219,13 +220,16 @@ class CSSFlattener(object):
|
||||
esize = 1
|
||||
if esize > 7:
|
||||
esize = 7
|
||||
cssdict['font-size'] = fnums[esize]
|
||||
font_size = fnums[esize]
|
||||
else:
|
||||
try:
|
||||
cssdict['font-size'] = fnums[force_int(size)]
|
||||
font_size = fnums[force_int(size)]
|
||||
except:
|
||||
cssdict['font-size'] = fnums[3]
|
||||
font_size = fnums[3]
|
||||
cssdict['font-size'] = '%.1fpt'%font_size
|
||||
del node.attrib['size']
|
||||
if 'face' in node.attrib:
|
||||
del node.attrib['face']
|
||||
if 'color' in node.attrib:
|
||||
cssdict['color'] = node.attrib['color']
|
||||
del node.attrib['color']
|
||||
@ -244,7 +248,7 @@ class CSSFlattener(object):
|
||||
cssdict['font-size'] = '%0.5fem'%(fsize/psize)
|
||||
psize = fsize
|
||||
elif 'font-size' in cssdict or tag == 'body':
|
||||
fsize = self.fmap[style['font-size']]
|
||||
fsize = self.fmap[font_size]
|
||||
cssdict['font-size'] = "%0.5fem" % (fsize / psize)
|
||||
psize = fsize
|
||||
if cssdict:
|
||||
|
@ -6,9 +6,9 @@ from threading import RLock
|
||||
|
||||
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||
QEvent, QTimer, pyqtSignal
|
||||
QEvent, QTimer, pyqtSignal, QDate
|
||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||
QIcon, QTableView, QApplication, QDialog, QPushButton
|
||||
QIcon, QApplication, QDialog, QPushButton
|
||||
|
||||
ORG_NAME = 'KovidsBrain'
|
||||
APP_UID = 'libprs500'
|
||||
@ -17,12 +17,14 @@ from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||
from calibre.utils.localization import set_qt_translator
|
||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.utils.date import UNDEFINED_DATE
|
||||
|
||||
gprefs = JSONConfig('gui')
|
||||
|
||||
NONE = QVariant() #: Null value to return from the data function of item models
|
||||
UNDEFINED_QDATE = QDate(UNDEFINED_DATE)
|
||||
|
||||
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher',
|
||||
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
|
||||
'tags', 'series', 'pubdate']
|
||||
|
||||
def _config():
|
||||
@ -95,7 +97,8 @@ def _config():
|
||||
help=_('Overwrite author and title with new metadata'))
|
||||
c.add_opt('enforce_cpu_limit', default=True,
|
||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||
|
||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||
help=_('tag browser categories not to display'))
|
||||
return ConfigProxy(c)
|
||||
|
||||
config = _config()
|
||||
@ -182,29 +185,38 @@ class MessageBox(QMessageBox):
|
||||
|
||||
|
||||
|
||||
def warning_dialog(parent, title, msg, det_msg='', show=False):
|
||||
def warning_dialog(parent, title, msg, det_msg='', show=False,
|
||||
show_copy_button=True):
|
||||
d = MessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok,
|
||||
parent, det_msg)
|
||||
d.setEscapeButton(QMessageBox.Ok)
|
||||
d.setIconPixmap(QPixmap(I('dialog_warning.svg')))
|
||||
if not show_copy_button:
|
||||
d.cb.setVisible(False)
|
||||
if show:
|
||||
return d.exec_()
|
||||
return d
|
||||
|
||||
def error_dialog(parent, title, msg, det_msg='', show=False):
|
||||
def error_dialog(parent, title, msg, det_msg='', show=False,
|
||||
show_copy_button=True):
|
||||
d = MessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok,
|
||||
parent, det_msg)
|
||||
d.setIconPixmap(QPixmap(I('dialog_error.svg')))
|
||||
d.setEscapeButton(QMessageBox.Ok)
|
||||
if not show_copy_button:
|
||||
d.cb.setVisible(False)
|
||||
if show:
|
||||
return d.exec_()
|
||||
return d
|
||||
|
||||
def question_dialog(parent, title, msg, det_msg=''):
|
||||
def question_dialog(parent, title, msg, det_msg='', show_copy_button=True):
|
||||
d = MessageBox(QMessageBox.Question, title, msg, QMessageBox.Yes|QMessageBox.No,
|
||||
parent, det_msg)
|
||||
d.setIconPixmap(QPixmap(I('dialog_information.svg')))
|
||||
d.setEscapeButton(QMessageBox.No)
|
||||
if not show_copy_button:
|
||||
d.cb.setVisible(False)
|
||||
|
||||
return d.exec_() == QMessageBox.Yes
|
||||
|
||||
def info_dialog(parent, title, msg, det_msg='', show=False):
|
||||
@ -216,24 +228,6 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
|
||||
return d
|
||||
|
||||
|
||||
def qstring_to_unicode(q):
|
||||
return unicode(q)
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
divisor, suffix = 1, "B"
|
||||
if size < 1024*1024:
|
||||
divisor, suffix = 1024., "KB"
|
||||
elif size < 1024*1024*1024:
|
||||
divisor, suffix = 1024*1024, "MB"
|
||||
elif size < 1024*1024*1024*1024:
|
||||
divisor, suffix = 1024*1024*1024, "GB"
|
||||
size = str(float(size)/divisor)
|
||||
if size.find(".") > -1:
|
||||
size = size[:size.find(".")+2]
|
||||
if size.endswith('.0'):
|
||||
size = size[:-2]
|
||||
return size + " " + suffix
|
||||
|
||||
class Dispatcher(QObject):
|
||||
'''Convenience class to ensure that a function call always happens in the
|
||||
@ -286,25 +280,6 @@ class GetMetadata(QObject):
|
||||
mi = MetaInformation('', [_('Unknown')])
|
||||
self.emit(SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'), id, mi)
|
||||
|
||||
class TableView(QTableView):
|
||||
|
||||
def __init__(self, parent):
|
||||
QTableView.__init__(self, parent)
|
||||
self.read_settings()
|
||||
|
||||
def read_settings(self):
|
||||
self.cw = dynamic[self.__class__.__name__+'column widths']
|
||||
|
||||
def write_settings(self):
|
||||
dynamic[self.__class__.__name__+'column widths'] = \
|
||||
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
|
||||
|
||||
def restore_column_widths(self):
|
||||
if self.cw and len(self.cw):
|
||||
for i in range(len(self.cw)):
|
||||
self.setColumnWidth(i, self.cw[i])
|
||||
return True
|
||||
|
||||
class FileIconProvider(QFileIconProvider):
|
||||
|
||||
ICONS = {
|
||||
@ -379,7 +354,7 @@ class FileIconProvider(QFileIconProvider):
|
||||
if fileinfo.isDir():
|
||||
key = 'dir'
|
||||
else:
|
||||
ext = qstring_to_unicode(fileinfo.completeSuffix()).lower()
|
||||
ext = unicode(fileinfo.completeSuffix()).lower()
|
||||
key = self.key_from_ext(ext)
|
||||
return self.cached_icon(key)
|
||||
|
||||
|
@ -181,6 +181,8 @@ class DBAdder(Thread):
|
||||
mi.title = os.path.splitext(name)[0]
|
||||
mi.title = mi.title if isinstance(mi.title, unicode) else \
|
||||
mi.title.decode(preferred_encoding, 'replace')
|
||||
if mi.application_id == '__calibre_dummy__':
|
||||
mi.application_id = None
|
||||
if self.db is not None:
|
||||
if cover:
|
||||
cover = open(cover, 'rb').read()
|
||||
@ -220,6 +222,8 @@ class DBAdder(Thread):
|
||||
|
||||
class Adder(QObject):
|
||||
|
||||
ADD_TIMEOUT = 600 # seconds
|
||||
|
||||
def __init__(self, parent, db, callback, spare_server=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.pd = ProgressDialog(_('Adding...'), parent=parent)
|
||||
@ -326,7 +330,7 @@ class Adder(QObject):
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
if (time.time() - self.last_added_at) > 300:
|
||||
if (time.time() - self.last_added_at) > self.ADD_TIMEOUT:
|
||||
self.timer.stop()
|
||||
self.pd.hide()
|
||||
self.db_adder.end = True
|
||||
|
174
src/calibre/gui2/add_wizard/__init__.py
Normal file
@ -0,0 +1,174 @@
|
||||
#!/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 os
|
||||
|
||||
from PyQt4.Qt import QWizard, QWizardPage, QIcon, QPixmap, Qt, QThread, \
|
||||
pyqtSignal
|
||||
|
||||
from calibre.gui2 import error_dialog, choose_dir, gprefs
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.library.add_to_library import find_folders_under, \
|
||||
find_books_in_folder, hash_merge_format_collections
|
||||
|
||||
class WizardPage(QWizardPage): # {{{
|
||||
|
||||
def __init__(self, db, parent):
|
||||
QWizardPage.__init__(self, parent)
|
||||
self.db = db
|
||||
self.register = parent.register
|
||||
self.setupUi(self)
|
||||
|
||||
self.do_init()
|
||||
|
||||
def do_init(self):
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
# Scan root folder Page {{{
|
||||
|
||||
from calibre.gui2.add_wizard.scan_ui import Ui_WizardPage as ScanWidget
|
||||
|
||||
class RecursiveFinder(QThread):
|
||||
|
||||
activity_changed = pyqtSignal(object, object) # description and total count
|
||||
activity_iterated = pyqtSignal(object, object) # item desc, progress number
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QThread.__init__(self, parent)
|
||||
self.canceled = False
|
||||
self.cancel_callback = lambda : self.canceled
|
||||
self.folders = set([])
|
||||
self.books = []
|
||||
|
||||
def cancel(self, *args):
|
||||
self.canceled = True
|
||||
|
||||
def set_params(self, root, db, one_per_folder):
|
||||
self.root, self.db = root, db
|
||||
self.one_per_folder = one_per_folder
|
||||
|
||||
def run(self):
|
||||
self.activity_changed.emit(_('Searching for sub-folders'), 0)
|
||||
self.folders = find_folders_under(self.root, self.db,
|
||||
cancel_callback=self.cancel_callback)
|
||||
if self.canceled:
|
||||
return
|
||||
self.activity_changed.emit(_('Searching for books'), len(self.folders))
|
||||
for i, folder in enumerate(self.folders):
|
||||
if self.canceled:
|
||||
break
|
||||
books_in_folder = find_books_in_folder(folder, self.one_per_folder,
|
||||
cancel_callback=self.cancel_callback)
|
||||
if self.canceled:
|
||||
break
|
||||
self.books.extend(books_in_folder)
|
||||
self.activity_iterated.emit(folder, i)
|
||||
|
||||
self.activity_changed.emit(
|
||||
_('Looking for duplicates based on file hash'), 0)
|
||||
|
||||
self.books = hash_merge_format_collections(self.books,
|
||||
cancel_callback=self.cancel_callback)
|
||||
|
||||
|
||||
|
||||
class ScanPage(WizardPage, ScanWidget):
|
||||
|
||||
ID = 2
|
||||
|
||||
# }}}
|
||||
|
||||
# Welcome Page {{{
|
||||
|
||||
from calibre.gui2.add_wizard.welcome_ui import Ui_WizardPage as WelcomeWidget
|
||||
|
||||
class WelcomePage(WizardPage, WelcomeWidget):
|
||||
|
||||
ID = 1
|
||||
|
||||
def do_init(self):
|
||||
# Root folder must be filled
|
||||
self.registerField('root_folder*', self.opt_root_folder)
|
||||
|
||||
self.register['root_folder'] = self.get_root_folder
|
||||
self.register['one_per_folder'] = self.get_one_per_folder
|
||||
|
||||
self.button_choose_root_folder.clicked.connect(self.choose_root_folder)
|
||||
|
||||
def choose_root_folder(self, *args):
|
||||
x = self.get_root_folder()
|
||||
if x is None:
|
||||
x = '~'
|
||||
x = choose_dir(self, 'add wizard choose root folder',
|
||||
_('Choose root folder'), default_dir=x)
|
||||
if x is not None:
|
||||
self.opt_root_folder.setText(os.path.abspath(x))
|
||||
|
||||
def initializePage(self):
|
||||
opf = gprefs.get('add wizard one per folder', True)
|
||||
self.opt_one_per_folder.setChecked(opf)
|
||||
self.opt_many_per_folder.setChecked(not opf)
|
||||
add_dir = gprefs.get('add wizard root folder', None)
|
||||
if add_dir is not None:
|
||||
self.opt_root_folder.setText(add_dir)
|
||||
|
||||
def get_root_folder(self):
|
||||
x = unicode(self.opt_root_folder.text()).strip()
|
||||
if not x:
|
||||
return None
|
||||
return os.path.abspath(x.encode(filesystem_encoding))
|
||||
|
||||
def get_one_per_folder(self):
|
||||
return self.opt_one_per_folder.isChecked()
|
||||
|
||||
def validatePage(self):
|
||||
x = self.get_root_folder()
|
||||
xu = x.decode(filesystem_encoding)
|
||||
if x and os.access(x, os.R_OK) and os.path.isdir(x):
|
||||
gprefs['add wizard root folder'] = xu
|
||||
gprefs['add wizard one per folder'] = self.get_one_per_folder()
|
||||
return True
|
||||
error_dialog(self, _('Invalid root folder'),
|
||||
xu + _('is not a valid root folder'), show=True)
|
||||
return False
|
||||
|
||||
# }}}
|
||||
|
||||
class Wizard(QWizard): # {{{
|
||||
|
||||
def __init__(self, db, parent=None):
|
||||
QWizard.__init__(self, parent)
|
||||
self.setModal(True)
|
||||
self.setWindowTitle(_('Add books to calibre'))
|
||||
self.setWindowIcon(QIcon(I('add_book.svg')))
|
||||
self.setPixmap(self.LogoPixmap, QPixmap(P('content_server/calibre.png')).scaledToHeight(80,
|
||||
Qt.SmoothTransformation))
|
||||
self.setPixmap(self.WatermarkPixmap,
|
||||
QPixmap(I('welcome_wizard.svg')))
|
||||
|
||||
self.register = {}
|
||||
|
||||
for attr, cls in [
|
||||
('welcome_page', WelcomePage),
|
||||
('scan_page', ScanPage),
|
||||
]:
|
||||
setattr(self, attr, cls(db, self))
|
||||
self.setPage(getattr(cls, 'ID'), getattr(self, attr))
|
||||
|
||||
# }}}
|
||||
|
||||
# Test Wizard {{{
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
from calibre.library import db
|
||||
app = QApplication([])
|
||||
w = Wizard(db())
|
||||
w.exec_()
|
||||
# }}}
|
||||
|
25
src/calibre/gui2/add_wizard/scan.ui
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>WizardPage</class>
|
||||
<widget class="QWizardPage" name="WizardPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>WizardPage</string>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Scanning root folder for books</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>This may take a few minutes</string>
|
||||
</property>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
134
src/calibre/gui2/add_wizard/welcome.ui
Normal file
@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>WizardPage</class>
|
||||
<widget class="QWizardPage" name="WizardPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>704</width>
|
||||
<height>468</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>WizardPage</string>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Choose the location to add books from</string>
|
||||
</property>
|
||||
<property name="subTitle">
|
||||
<string>Select a folder on your hard disk</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string><p>calibre can scan your computer for existing books automatically. These books will then be <b>copied</b> into the calibre library. This wizard will help you customize the scanning and import process for your existing book collection.</p>
|
||||
<p>Choose a root folder. Books will be searched for only inside this folder and any sub-folders.</p>
|
||||
<p>Make sure that the folder you chose for your calibre library <b>is not</b> under the root folder you choose.</p></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Root folder:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_root_folder</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="opt_root_folder">
|
||||
<property name="toolTip">
|
||||
<string>This folder and its sub-folders will be scanned for books to import into calibre's library</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QToolButton" name="button_choose_root_folder">
|
||||
<property name="toolTip">
|
||||
<string>Choose root folder</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="3">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Handle multiple files per book</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="opt_one_per_folder">
|
||||
<property name="text">
|
||||
<string>&One book per folder, assumes every ebook file in a folder is the same book in a different format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="opt_many_per_folder">
|
||||
<property name="text">
|
||||
<string>&Multiple books per folder, assumes every ebook file is a different book</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
@ -14,6 +14,7 @@ from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
|
||||
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
|
||||
from calibre.gui2 import error_dialog, choose_files
|
||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||
from calibre.ebooks.conversion.preprocess import convert_entities
|
||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||
|
||||
class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||
@ -87,8 +88,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
||||
self.iterator = EbookIterator(pathtoebook)
|
||||
self.iterator.__enter__(only_input_plugin=True)
|
||||
text = [u'']
|
||||
ent_pat = re.compile(r'&(\S+?);')
|
||||
for path in self.iterator.spine:
|
||||
html = open(path, 'rb').read().decode('utf-8', 'replace')
|
||||
html = ent_pat.sub(convert_entities, html)
|
||||
text.append(html)
|
||||
self.preview.setPlainText('\n---\n'.join(text))
|
||||
|
||||
|
@ -14,6 +14,7 @@ class PluginWidget(Widget, Ui_Form):
|
||||
|
||||
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
|
||||
Widget.__init__(self, parent, 'txt_input',
|
||||
['single_line_paras', 'print_formatted_paras', 'markdown', 'markdown_disable_toc'])
|
||||
['single_line_paras', 'print_formatted_paras', 'markdown',
|
||||
'markdown_disable_toc', 'preserve_spaces'])
|
||||
self.db, self.book_id = db, book_id
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<width>470</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -52,7 +52,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -65,6 +65,13 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="opt_preserve_spaces">
|
||||
<property name="text">
|
||||
<string>Preserve &spaces</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
460
src/calibre/gui2/custom_column_widgets.py
Normal file
@ -0,0 +1,460 @@
|
||||
#!/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 sys
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
|
||||
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
|
||||
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL
|
||||
|
||||
from calibre.utils.date import qt_to_dt
|
||||
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
|
||||
from calibre.gui2 import UNDEFINED_QDATE
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
class Base(object):
|
||||
|
||||
def __init__(self, db, col_id, parent=None):
|
||||
self.db, self.col_id = db, col_id
|
||||
self.col_metadata = db.custom_column_num_map[col_id]
|
||||
self.initial_val = None
|
||||
self.setup_ui(parent)
|
||||
|
||||
def initialize(self, book_id):
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
self.initial_val = val
|
||||
val = self.normalize_db_val(val)
|
||||
self.setter(val)
|
||||
|
||||
def commit(self, book_id, notify=False):
|
||||
val = self.getter()
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val:
|
||||
self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
|
||||
|
||||
def normalize_db_val(self, val):
|
||||
return val
|
||||
|
||||
def normalize_ui_val(self, val):
|
||||
return val
|
||||
|
||||
class Bool(Base):
|
||||
|
||||
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.svg'), I('list_remove.svg'), I('blank.svg')]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
items = items[:-1]
|
||||
icons = icons[:-1]
|
||||
for icon, text in zip(icons, items):
|
||||
w.addItem(QIcon(icon), text)
|
||||
|
||||
def setter(self, val):
|
||||
val = {None: 2, False: 1, True: 0}[val]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val == 2:
|
||||
val = 1
|
||||
self.widgets[1].setCurrentIndex(val)
|
||||
|
||||
def getter(self):
|
||||
val = self.widgets[1].currentIndex()
|
||||
return {2: None, 1: False, 0: True}[val]
|
||||
|
||||
class Int(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QSpinBox(parent)]
|
||||
w = self.widgets[1]
|
||||
w.setRange(-100, sys.maxint)
|
||||
w.setSpecialValueText(_('Undefined'))
|
||||
w.setSingleStep(1)
|
||||
|
||||
def setter(self, val):
|
||||
if val is None:
|
||||
val = self.widgets[1].minimum()
|
||||
else:
|
||||
val = int(val)
|
||||
self.widgets[1].setValue(val)
|
||||
|
||||
def getter(self):
|
||||
val = self.widgets[1].value()
|
||||
if val == self.widgets[1].minimum():
|
||||
val = None
|
||||
return val
|
||||
|
||||
class Float(Int):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
QDoubleSpinBox(parent)]
|
||||
w = self.widgets[1]
|
||||
w.setRange(-100., float(sys.maxint))
|
||||
w.setDecimals(2)
|
||||
w.setSpecialValueText(_('Undefined'))
|
||||
w.setSingleStep(1)
|
||||
|
||||
class Rating(Int):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
Int.setup_ui(self, parent)
|
||||
w = self.widgets[1]
|
||||
w.setRange(0, 5)
|
||||
w.setSuffix(' '+_('star(s)'))
|
||||
w.setSpecialValueText(_('Unrated'))
|
||||
|
||||
def setter(self, val):
|
||||
if val is None:
|
||||
val = 0
|
||||
self.widgets[1].setValue(int(round(val/2.)))
|
||||
|
||||
def getter(self):
|
||||
val = self.widgets[1].value()
|
||||
if val == 0:
|
||||
val = None
|
||||
else:
|
||||
val *= 2
|
||||
return val
|
||||
|
||||
class DateEdit(QDateEdit):
|
||||
|
||||
def focusInEvent(self, x):
|
||||
self.setSpecialValueText('')
|
||||
|
||||
def focusOutEvent(self, x):
|
||||
self.setSpecialValueText(_('Undefined'))
|
||||
|
||||
class DateTime(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
DateEdit(parent)]
|
||||
w = self.widgets[1]
|
||||
w.setDisplayFormat('dd MMM yyyy')
|
||||
w.setCalendarPopup(True)
|
||||
w.setMinimumDate(UNDEFINED_QDATE)
|
||||
w.setSpecialValueText(_('Undefined'))
|
||||
|
||||
def setter(self, val):
|
||||
if val is None:
|
||||
val = self.widgets[1].minimumDate()
|
||||
else:
|
||||
val = QDate(val.year, val.month, val.day)
|
||||
self.widgets[1].setDate(val)
|
||||
|
||||
def getter(self):
|
||||
val = self.widgets[1].date()
|
||||
if val == UNDEFINED_QDATE:
|
||||
val = None
|
||||
else:
|
||||
val = qt_to_dt(val)
|
||||
return val
|
||||
|
||||
|
||||
class Comments(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
self._box = QGroupBox(parent)
|
||||
self._box.setTitle('&'+self.col_metadata['name'])
|
||||
self._layout = QVBoxLayout()
|
||||
self._tb = QPlainTextEdit(self._box)
|
||||
self._tb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self._tb.setTabChangesFocus(True)
|
||||
self._layout.addWidget(self._tb)
|
||||
self._box.setLayout(self._layout)
|
||||
self.widgets = [self._box]
|
||||
|
||||
def setter(self, val):
|
||||
if val is None:
|
||||
val = ''
|
||||
self._tb.setPlainText(val)
|
||||
|
||||
def getter(self):
|
||||
val = unicode(self._tb.toPlainText()).strip()
|
||||
if not val:
|
||||
val = None
|
||||
return val
|
||||
|
||||
class Text(Base):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
|
||||
if self.col_metadata['is_multiple']:
|
||||
w = TagsLineEdit(parent, values)
|
||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
else:
|
||||
w = EnComboBox(parent)
|
||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||
w.setMinimumContentsLength(25)
|
||||
|
||||
|
||||
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
||||
w]
|
||||
|
||||
def initialize(self, book_id):
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
self.initial_val = val
|
||||
val = self.normalize_db_val(val)
|
||||
if self.col_metadata['is_multiple']:
|
||||
self.setter(val)
|
||||
self.widgets[1].update_tags_cache(self.all_values)
|
||||
else:
|
||||
idx = None
|
||||
for i, c in enumerate(self.all_values):
|
||||
if c == val:
|
||||
idx = i
|
||||
self.widgets[1].addItem(c)
|
||||
self.widgets[1].setEditText('')
|
||||
if idx is not None:
|
||||
self.widgets[1].setCurrentIndex(idx)
|
||||
|
||||
|
||||
def setter(self, val):
|
||||
if self.col_metadata['is_multiple']:
|
||||
if not val:
|
||||
val = []
|
||||
self.widgets[1].setText(u', '.join(val))
|
||||
|
||||
def getter(self):
|
||||
if self.col_metadata['is_multiple']:
|
||||
val = unicode(self.widgets[1].text()).strip()
|
||||
ans = [x.strip() for x in val.split(',') if x.strip()]
|
||||
if not ans:
|
||||
ans = None
|
||||
return ans
|
||||
val = unicode(self.widgets[1].currentText()).strip()
|
||||
if not val:
|
||||
val = None
|
||||
return val
|
||||
|
||||
widgets = {
|
||||
'bool' : Bool,
|
||||
'rating' : Rating,
|
||||
'int': Int,
|
||||
'float': Float,
|
||||
'datetime': DateTime,
|
||||
'text' : Text,
|
||||
'comments': Comments,
|
||||
}
|
||||
|
||||
def field_sort(y, z, x=None):
|
||||
m1, m2 = x[y], x[z]
|
||||
n1 = 'zzzzz' if m1['datatype'] == 'comments' else m1['name']
|
||||
n2 = 'zzzzz' if m2['datatype'] == 'comments' else m2['name']
|
||||
return cmp(n1.lower(), n2.lower())
|
||||
|
||||
def populate_single_metadata_page(left, right, db, book_id, parent=None):
|
||||
x = db.custom_column_num_map
|
||||
cols = list(x)
|
||||
cols.sort(cmp=partial(field_sort, x=x))
|
||||
ans = []
|
||||
for i, col in enumerate(cols):
|
||||
w = widgets[x[col]['datatype']](db, col, parent)
|
||||
ans.append(w)
|
||||
w.initialize(book_id)
|
||||
layout = left if i%2 == 0 else right
|
||||
row = layout.rowCount()
|
||||
if len(w.widgets) == 1:
|
||||
layout.addWidget(w.widgets[0], row, 0, 1, -1)
|
||||
else:
|
||||
w.widgets[0].setBuddy(w.widgets[1])
|
||||
for c, widget in enumerate(w.widgets):
|
||||
layout.addWidget(widget, row, c)
|
||||
items = []
|
||||
if len(ans) > 0:
|
||||
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
||||
QSizePolicy.Expanding))
|
||||
left.addItem(items[-1], left.rowCount(), 0, 1, 1)
|
||||
left.setRowStretch(left.rowCount()-1, 100)
|
||||
if len(ans) > 1:
|
||||
items.append(QSpacerItem(10, 100, QSizePolicy.Minimum,
|
||||
QSizePolicy.Expanding))
|
||||
right.addItem(items[-1], left.rowCount(), 0, 1, 1)
|
||||
right.setRowStretch(right.rowCount()-1, 100)
|
||||
|
||||
return ans, items
|
||||
|
||||
class BulkBase(Base):
|
||||
|
||||
def get_initial_value(self, book_ids):
|
||||
values = set([])
|
||||
for book_id in book_ids:
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
if isinstance(val, list):
|
||||
val = frozenset(val)
|
||||
values.add(val)
|
||||
if len(values) > 1:
|
||||
break
|
||||
ans = None
|
||||
if len(values) == 1:
|
||||
ans = iter(values).next()
|
||||
if isinstance(ans, frozenset):
|
||||
ans = list(ans)
|
||||
return ans
|
||||
|
||||
def process_each_book(self):
|
||||
return False
|
||||
|
||||
def initialize(self, book_ids):
|
||||
if not self.process_each_book():
|
||||
self.initial_val = val = self.get_initial_value(book_ids)
|
||||
val = self.normalize_db_val(val)
|
||||
self.setter(val)
|
||||
|
||||
def commit(self, book_ids, notify=False):
|
||||
if self.process_each_book():
|
||||
for book_id in book_ids:
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
self.db.set_custom(book_id, self.getter(val), num=self.col_id, notify=notify)
|
||||
else:
|
||||
val = self.getter()
|
||||
val = self.normalize_ui_val(val)
|
||||
if val != self.initial_val:
|
||||
for book_id in book_ids:
|
||||
self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
|
||||
|
||||
class BulkBool(BulkBase, Bool):
|
||||
pass
|
||||
|
||||
class BulkInt(BulkBase, Int):
|
||||
pass
|
||||
|
||||
class BulkFloat(BulkBase, Float):
|
||||
pass
|
||||
|
||||
class BulkRating(BulkBase, Rating):
|
||||
pass
|
||||
|
||||
class BulkDateTime(BulkBase, DateTime):
|
||||
pass
|
||||
|
||||
class RemoveTags(QWidget):
|
||||
|
||||
def __init__(self, parent, values):
|
||||
QWidget.__init__(self, parent)
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(5)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.tags_box = TagsLineEdit(parent, values)
|
||||
layout.addWidget(self.tags_box, stretch = 1)
|
||||
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
|
||||
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
||||
layout.addWidget(self.checkbox)
|
||||
self.setLayout(layout)
|
||||
self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched)
|
||||
|
||||
def box_touched(self, state):
|
||||
if state:
|
||||
self.tags_box.setText('')
|
||||
self.tags_box.setEnabled(False)
|
||||
else:
|
||||
self.tags_box.setEnabled(True)
|
||||
|
||||
class BulkText(BulkBase):
|
||||
|
||||
def setup_ui(self, parent):
|
||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
|
||||
if self.col_metadata['is_multiple']:
|
||||
w = TagsLineEdit(parent, values)
|
||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' +
|
||||
_('tags to add'), parent), w]
|
||||
self.adding_widget = w
|
||||
|
||||
w = RemoveTags(parent, values)
|
||||
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
||||
_('tags to remove'), parent))
|
||||
self.widgets.append(w)
|
||||
self.removing_widget = w
|
||||
else:
|
||||
w = EnComboBox(parent)
|
||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
||||
w.setMinimumContentsLength(25)
|
||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
||||
|
||||
def initialize(self, book_ids):
|
||||
if self.col_metadata['is_multiple']:
|
||||
self.widgets[1].update_tags_cache(self.all_values)
|
||||
else:
|
||||
val = self.get_initial_value(book_ids)
|
||||
self.initial_val = val = self.normalize_db_val(val)
|
||||
idx = None
|
||||
for i, c in enumerate(self.all_values):
|
||||
if c == val:
|
||||
idx = i
|
||||
self.widgets[1].addItem(c)
|
||||
self.widgets[1].setEditText('')
|
||||
if idx is not None:
|
||||
self.widgets[1].setCurrentIndex(idx)
|
||||
|
||||
def process_each_book(self):
|
||||
return self.col_metadata['is_multiple']
|
||||
|
||||
def getter(self, original_value = None):
|
||||
if self.col_metadata['is_multiple']:
|
||||
if self.removing_widget.checkbox.isChecked():
|
||||
ans = set()
|
||||
else:
|
||||
ans = set(original_value)
|
||||
ans -= set([v.strip() for v in
|
||||
unicode(self.removing_widget.tags_box.text()).split(',')])
|
||||
ans |= set([v.strip() for v in
|
||||
unicode(self.adding_widget.text()).split(',')])
|
||||
return ans # returning a set instead of a list works, for now at least.
|
||||
val = unicode(self.widgets[1].currentText()).strip()
|
||||
if not val:
|
||||
val = None
|
||||
return val
|
||||
|
||||
|
||||
bulk_widgets = {
|
||||
'bool' : BulkBool,
|
||||
'rating' : BulkRating,
|
||||
'int': BulkInt,
|
||||
'float': BulkFloat,
|
||||
'datetime': BulkDateTime,
|
||||
'text' : BulkText,
|
||||
}
|
||||
|
||||
def populate_bulk_metadata_page(layout, db, book_ids, parent=None):
|
||||
x = db.custom_column_num_map
|
||||
cols = list(x)
|
||||
cols.sort(cmp=partial(field_sort, x=x))
|
||||
ans = []
|
||||
for i, col in enumerate(cols):
|
||||
dt = x[col]['datatype']
|
||||
if dt == 'comments':
|
||||
continue
|
||||
w = bulk_widgets[dt](db, col, parent)
|
||||
ans.append(w)
|
||||
w.initialize(book_ids)
|
||||
row = layout.rowCount()
|
||||
if len(w.widgets) == 1:
|
||||
layout.addWidget(w.widgets[0], row, 0, 1, -1)
|
||||
else:
|
||||
for c in range(0, len(w.widgets), 2):
|
||||
w.widgets[c].setBuddy(w.widgets[c+1])
|
||||
layout.addWidget(w.widgets[c], row, 0)
|
||||
layout.addWidget(w.widgets[c+1], row, 1)
|
||||
row += 1
|
||||
items = []
|
||||
if len(ans) > 0:
|
||||
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
|
||||
QSizePolicy.Expanding))
|
||||
layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
|
||||
layout.setRowStretch(layout.rowCount()-1, 100)
|
||||
|
||||
return ans, items
|
||||
|
@ -1,7 +1,7 @@
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, traceback, Queue, time, socket, cStringIO
|
||||
import os, traceback, Queue, time, socket, cStringIO, re
|
||||
from threading import Thread, RLock
|
||||
from itertools import repeat
|
||||
from functools import partial
|
||||
@ -19,12 +19,13 @@ from calibre.devices.scanner import DeviceScanner
|
||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||
pixmap_to_data, warning_dialog, \
|
||||
question_dialog
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre import preferred_encoding
|
||||
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
||||
from calibre import preferred_encoding, prints
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.devices.errors import FreeSpaceError
|
||||
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
||||
config as email_config
|
||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
||||
|
||||
class DeviceJob(BaseJob):
|
||||
|
||||
@ -36,6 +37,7 @@ class DeviceJob(BaseJob):
|
||||
self.exception = None
|
||||
self.job_manager = job_manager
|
||||
self._details = _('No details available.')
|
||||
self._aborted = False
|
||||
|
||||
def start_work(self):
|
||||
self.start_time = time.time()
|
||||
@ -54,7 +56,11 @@ class DeviceJob(BaseJob):
|
||||
self.start_work()
|
||||
try:
|
||||
self.result = self.func(*self.args, **self.kwargs)
|
||||
if self._aborted:
|
||||
return
|
||||
except (Exception, SystemExit), err:
|
||||
if self._aborted:
|
||||
return
|
||||
self.failed = True
|
||||
self._details = unicode(err) + '\n\n' + \
|
||||
traceback.format_exc()
|
||||
@ -62,6 +68,12 @@ class DeviceJob(BaseJob):
|
||||
finally:
|
||||
self.job_done()
|
||||
|
||||
def abort(self, err):
|
||||
self._aborted = True
|
||||
self.failed = True
|
||||
self._details = unicode(err)
|
||||
self.exception = err
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return cStringIO.StringIO(self._details.encode('utf-8'))
|
||||
@ -69,7 +81,7 @@ class DeviceJob(BaseJob):
|
||||
|
||||
class DeviceManager(Thread):
|
||||
|
||||
def __init__(self, connected_slot, job_manager, sleep_time=2):
|
||||
def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2):
|
||||
'''
|
||||
:sleep_time: Time to sleep between device probes in secs
|
||||
'''
|
||||
@ -86,6 +98,9 @@ class DeviceManager(Thread):
|
||||
self.scanner = DeviceScanner()
|
||||
self.connected_device = None
|
||||
self.ejected_devices = set([])
|
||||
self.connected_device_is_folder = False
|
||||
self.folder_connection_requests = Queue.Queue(0)
|
||||
self.open_feedback_slot = open_feedback_slot
|
||||
|
||||
def report_progress(self, *args):
|
||||
pass
|
||||
@ -98,8 +113,10 @@ class DeviceManager(Thread):
|
||||
def device(self):
|
||||
return self.connected_device
|
||||
|
||||
def do_connect(self, connected_devices):
|
||||
def do_connect(self, connected_devices, is_folder_device):
|
||||
for dev, detected_device in connected_devices:
|
||||
if dev.OPEN_FEEDBACK_MESSAGE is not None:
|
||||
self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE)
|
||||
dev.reset(detected_device=detected_device,
|
||||
report_progress=self.report_progress)
|
||||
try:
|
||||
@ -109,7 +126,8 @@ class DeviceManager(Thread):
|
||||
traceback.print_exc()
|
||||
continue
|
||||
self.connected_device = dev
|
||||
self.connected_slot(True)
|
||||
self.connected_device_is_folder = is_folder_device
|
||||
self.connected_slot(True, is_folder_device)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -127,7 +145,7 @@ class DeviceManager(Thread):
|
||||
if self.connected_device in self.ejected_devices:
|
||||
self.ejected_devices.remove(self.connected_device)
|
||||
else:
|
||||
self.connected_slot(False)
|
||||
self.connected_slot(False, self.connected_device_is_folder)
|
||||
self.connected_device = None
|
||||
|
||||
def detect_device(self):
|
||||
@ -148,17 +166,19 @@ class DeviceManager(Thread):
|
||||
if possibly_connected:
|
||||
possibly_connected_devices.append((device, detected_device))
|
||||
if possibly_connected_devices:
|
||||
if not self.do_connect(possibly_connected_devices):
|
||||
if not self.do_connect(possibly_connected_devices,
|
||||
is_folder_device=False):
|
||||
print 'Connect to device failed, retrying in 5 seconds...'
|
||||
time.sleep(5)
|
||||
if not self.do_connect(possibly_connected_devices):
|
||||
if not self.do_connect(possibly_connected_devices,
|
||||
is_folder_device=False):
|
||||
print 'Device connect failed again, giving up'
|
||||
|
||||
def umount_device(self, *args):
|
||||
if self.is_device_connected:
|
||||
if self.is_device_connected and not self.job_manager.has_device_jobs():
|
||||
self.connected_device.eject()
|
||||
self.ejected_devices.add(self.connected_device)
|
||||
self.connected_slot(False)
|
||||
self.connected_slot(False, self.connected_device_is_folder)
|
||||
|
||||
def next(self):
|
||||
if not self.jobs.empty():
|
||||
@ -169,6 +189,22 @@ class DeviceManager(Thread):
|
||||
|
||||
def run(self):
|
||||
while self.keep_going:
|
||||
folder_path = None
|
||||
while True:
|
||||
try:
|
||||
folder_path = self.folder_connection_requests.get_nowait()
|
||||
except Queue.Empty:
|
||||
break
|
||||
if not folder_path or not os.access(folder_path, os.R_OK):
|
||||
folder_path = None
|
||||
if not self.is_device_connected and folder_path is not None:
|
||||
try:
|
||||
dev = FOLDER_DEVICE(folder_path)
|
||||
self.do_connect([[dev, None],], is_folder_device=True)
|
||||
except:
|
||||
prints('Unable to open folder as device', folder_path)
|
||||
traceback.print_exc()
|
||||
else:
|
||||
self.detect_device()
|
||||
while True:
|
||||
job = self.next()
|
||||
@ -181,7 +217,6 @@ class DeviceManager(Thread):
|
||||
break
|
||||
time.sleep(self.sleep_time)
|
||||
|
||||
|
||||
def create_job(self, func, done, description, args=[], kwargs={}):
|
||||
job = DeviceJob(func, done, self.job_manager,
|
||||
args=args, kwargs=kwargs, description=description)
|
||||
@ -207,6 +242,21 @@ class DeviceManager(Thread):
|
||||
return self.create_job(self._get_device_information, done,
|
||||
description=_('Get device information'))
|
||||
|
||||
# This will be called on the GUI thread. Because of this, we must store
|
||||
# information that the scanner thread will use to do the real work.
|
||||
def connect_to_folder(self, path):
|
||||
self.folder_connection_requests.put(path)
|
||||
|
||||
# This is called on the GUI thread. No problem here, because it calls the
|
||||
# device driver, telling it to tell the scanner when it passes by that the
|
||||
# folder has disconnected.
|
||||
def disconnect_folder(self):
|
||||
if self.connected_device is not None:
|
||||
if hasattr(self.connected_device, 'disconnect_from_folder'):
|
||||
# As we are on the wrong thread, this call must *not* do
|
||||
# anything besides set a flag that the right thread will see.
|
||||
self.connected_device.disconnect_from_folder()
|
||||
|
||||
def _books(self):
|
||||
'''Get metadata from device'''
|
||||
mainlist = self.device.books(oncard=None, end_session=False)
|
||||
@ -291,15 +341,17 @@ class DeviceManager(Thread):
|
||||
|
||||
class DeviceAction(QAction):
|
||||
|
||||
a_s = pyqtSignal(object)
|
||||
|
||||
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
|
||||
if delete:
|
||||
text += ' ' + _('and delete from library')
|
||||
QAction.__init__(self, QIcon(icon_path), text, parent)
|
||||
self.dest = dest
|
||||
self.delete = delete
|
||||
self.specific = specific
|
||||
self.connect(self, SIGNAL('triggered(bool)'),
|
||||
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
|
||||
self.triggered.connect(self.emit_triggered)
|
||||
|
||||
def emit_triggered(self, *args):
|
||||
self.a_s.emit(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
|
||||
@ -309,6 +361,8 @@ class DeviceAction(QAction):
|
||||
class DeviceMenu(QMenu):
|
||||
|
||||
fetch_annotations = pyqtSignal()
|
||||
connect_to_folder = pyqtSignal()
|
||||
disconnect_from_folder = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QMenu.__init__(self, parent)
|
||||
@ -316,8 +370,9 @@ class DeviceMenu(QMenu):
|
||||
self.actions = []
|
||||
self._memory = []
|
||||
|
||||
self.set_default_menu = self.addMenu(_('Set default send to device'
|
||||
' action'))
|
||||
self.set_default_menu = QMenu(_('Set default send to device action'))
|
||||
self.set_default_menu.setIcon(QIcon(I('config.svg')))
|
||||
|
||||
opts = email_config().parse()
|
||||
default_account = None
|
||||
if opts.accounts:
|
||||
@ -341,51 +396,65 @@ class DeviceMenu(QMenu):
|
||||
self.connect(action2, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
|
||||
_actions = [
|
||||
basic_actions = [
|
||||
('main:', False, False, I('reader.svg'),
|
||||
_('Send to main memory')),
|
||||
('carda:0', False, False, I('sd.svg'),
|
||||
_('Send to storage card A')),
|
||||
('cardb:0', False, False, I('sd.svg'),
|
||||
_('Send to storage card B')),
|
||||
'-----',
|
||||
('main:', True, False, I('reader.svg'),
|
||||
_('Send to main memory')),
|
||||
('carda:0', True, False, I('sd.svg'),
|
||||
_('Send to storage card A')),
|
||||
('cardb:0', True, False, I('sd.svg'),
|
||||
_('Send to storage card B')),
|
||||
'-----',
|
||||
('main:', False, True, I('reader.svg'),
|
||||
_('Send specific format to main memory')),
|
||||
('carda:0', False, True, I('sd.svg'),
|
||||
_('Send specific format to storage card A')),
|
||||
('cardb:0', False, True, I('sd.svg'),
|
||||
_('Send specific format to storage card B')),
|
||||
|
||||
]
|
||||
|
||||
delete_actions = [
|
||||
('main:', True, False, I('reader.svg'),
|
||||
_('Main Memory')),
|
||||
('carda:0', True, False, I('sd.svg'),
|
||||
_('Storage Card A')),
|
||||
('cardb:0', True, False, I('sd.svg'),
|
||||
_('Storage Card B')),
|
||||
]
|
||||
|
||||
specific_actions = [
|
||||
('main:', False, True, I('reader.svg'),
|
||||
_('Main Memory')),
|
||||
('carda:0', False, True, I('sd.svg'),
|
||||
_('Storage Card A')),
|
||||
('cardb:0', False, True, I('sd.svg'),
|
||||
_('Storage Card B')),
|
||||
]
|
||||
|
||||
|
||||
if default_account is not None:
|
||||
_actions.insert(2, default_account)
|
||||
_actions.insert(6, list(default_account))
|
||||
_actions[6][1] = True
|
||||
for round in (0, 1):
|
||||
for dest, delete, specific, icon, text in _actions:
|
||||
if dest == '-':
|
||||
(self.set_default_menu if round else self).addSeparator()
|
||||
continue
|
||||
for x in (basic_actions, delete_actions):
|
||||
ac = list(default_account)
|
||||
if x is delete_actions:
|
||||
ac[1] = True
|
||||
x.insert(1, tuple(ac))
|
||||
|
||||
for menu in (self, self.set_default_menu):
|
||||
for actions, desc in (
|
||||
(basic_actions, ''),
|
||||
(delete_actions, _('Send and delete from library')),
|
||||
(specific_actions, _('Send specific format'))
|
||||
):
|
||||
mdest = menu
|
||||
if actions is not basic_actions:
|
||||
mdest = menu.addMenu(desc)
|
||||
self._memory.append(mdest)
|
||||
|
||||
for dest, delete, specific, icon, text in actions:
|
||||
action = DeviceAction(dest, delete, specific, icon, text, self)
|
||||
self._memory.append(action)
|
||||
if round == 1:
|
||||
if menu is self.set_default_menu:
|
||||
action.setCheckable(True)
|
||||
action.setText(action.text())
|
||||
self.group.addAction(action)
|
||||
self.set_default_menu.addAction(action)
|
||||
else:
|
||||
self.connect(action, SIGNAL('a_s(QAction)'),
|
||||
self.action_triggered)
|
||||
action.a_s.connect(self.action_triggered)
|
||||
self.actions.append(action)
|
||||
self.addAction(action)
|
||||
|
||||
mdest.addAction(action)
|
||||
if actions is not specific_actions:
|
||||
menu.addSeparator()
|
||||
|
||||
da = config['default_send_to_device_action']
|
||||
done = False
|
||||
@ -399,11 +468,24 @@ class DeviceMenu(QMenu):
|
||||
action.setChecked(True)
|
||||
config['default_send_to_device_action'] = repr(action)
|
||||
|
||||
self.connect(self.group, SIGNAL('triggered(QAction*)'),
|
||||
self.change_default_action)
|
||||
self.group.triggered.connect(self.change_default_action)
|
||||
if opts.accounts:
|
||||
self.addSeparator()
|
||||
self.addMenu(self.email_to_menu)
|
||||
|
||||
self.addSeparator()
|
||||
mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder'))
|
||||
mitem.setEnabled(True)
|
||||
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
|
||||
self.connect_to_folder_action = mitem
|
||||
|
||||
mitem = self.addAction(QIcon(I('eject.svg')), _('Disconnect from folder'))
|
||||
mitem.setEnabled(False)
|
||||
mitem.triggered.connect(lambda x : self.disconnect_from_folder.emit())
|
||||
self.disconnect_from_folder_action = mitem
|
||||
|
||||
self.addSeparator()
|
||||
self.addMenu(self.set_default_menu)
|
||||
self.addSeparator()
|
||||
annot = self.addAction(_('Fetch annotations (experimental)'))
|
||||
annot.setEnabled(False)
|
||||
@ -523,6 +605,7 @@ class DeviceGUI(object):
|
||||
d = ChooseFormatDialog(self, _('Choose format to send to device'),
|
||||
self.device_manager.device.settings().format_map)
|
||||
d.exec_()
|
||||
if d.format():
|
||||
fmt = d.format().lower()
|
||||
dest, sub_dest = dest.split(':')
|
||||
if dest in ('main', 'carda', 'cardb'):
|
||||
@ -821,7 +904,9 @@ class DeviceGUI(object):
|
||||
|
||||
def sync_to_device(self, on_card, delete_from_library,
|
||||
specific_format=None, send_ids=None, do_auto_convert=True):
|
||||
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
||||
ids = [self.library_view.model().id(r) \
|
||||
for r in self.library_view.selectionModel().selectedRows()] \
|
||||
if send_ids is None else send_ids
|
||||
if not self.device_manager or not ids or len(ids) == 0:
|
||||
return
|
||||
|
||||
@ -842,8 +927,7 @@ class DeviceGUI(object):
|
||||
ids = iter(ids)
|
||||
for mi in metadata:
|
||||
if mi.cover and os.access(mi.cover, os.R_OK):
|
||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
|
||||
'rb').read())
|
||||
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover, 'rb').read())
|
||||
imetadata = iter(metadata)
|
||||
|
||||
files = [getattr(f, 'name', None) for f in _files]
|
||||
@ -890,7 +974,9 @@ class DeviceGUI(object):
|
||||
bad.append(self.library_view.model().db.title(id, index_is_id=True))
|
||||
|
||||
if auto != []:
|
||||
format = specific_format if specific_format in list(set(settings.format_map).intersection(set(available_output_formats()))) else None
|
||||
format = specific_format if specific_format in \
|
||||
list(set(settings.format_map).intersection(set(available_output_formats()))) \
|
||||
else None
|
||||
if not format:
|
||||
for fmt in settings.format_map:
|
||||
if fmt in list(set(settings.format_map).intersection(set(available_output_formats()))):
|
||||
@ -971,10 +1057,137 @@ class DeviceGUI(object):
|
||||
|
||||
self.upload_booklists()
|
||||
|
||||
books_to_be_deleted = []
|
||||
if memory and memory[1]:
|
||||
books_to_be_deleted = memory[1]
|
||||
self.library_view.model().delete_books_by_id(books_to_be_deleted)
|
||||
|
||||
self.set_books_in_library(self.booklists(),
|
||||
reset=bool(books_to_be_deleted))
|
||||
|
||||
view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view
|
||||
view.model().resort(reset=False)
|
||||
view.model().research()
|
||||
for f in files:
|
||||
getattr(f, 'close', lambda : True)()
|
||||
if memory and memory[1]:
|
||||
self.library_view.model().delete_books_by_id(memory[1])
|
||||
|
||||
self.book_on_device(None, reset=True)
|
||||
if metadata:
|
||||
changed = set([])
|
||||
for mi in metadata:
|
||||
id_ = getattr(mi, 'application_id', None)
|
||||
if id_ is not None:
|
||||
changed.add(id_)
|
||||
if changed:
|
||||
self.library_view.model().refresh_ids(list(changed))
|
||||
|
||||
def book_on_device(self, id, format=None, reset=False):
|
||||
loc = [None, None, None]
|
||||
|
||||
if reset:
|
||||
self.book_db_title_cache = None
|
||||
self.book_db_uuid_cache = None
|
||||
return
|
||||
|
||||
if self.book_db_title_cache is None:
|
||||
self.book_db_title_cache = []
|
||||
self.book_db_uuid_cache = []
|
||||
for i, l in enumerate(self.booklists()):
|
||||
self.book_db_title_cache.append({})
|
||||
self.book_db_uuid_cache.append(set())
|
||||
for book in l:
|
||||
book_title = book.title.lower() if book.title else ''
|
||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
||||
if book_title not in self.book_db_title_cache[i]:
|
||||
self.book_db_title_cache[i][book_title] = \
|
||||
{'authors':set(), 'db_ids':set(), 'uuids':set()}
|
||||
book_authors = authors_to_string(book.authors).lower()
|
||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
||||
self.book_db_title_cache[i][book_title]['authors'].add(book_authors)
|
||||
db_id = getattr(book, 'application_id', None)
|
||||
if db_id is None:
|
||||
db_id = book.db_id
|
||||
if db_id is not None:
|
||||
self.book_db_title_cache[i][book_title]['db_ids'].add(db_id)
|
||||
uuid = getattr(book, 'uuid', None)
|
||||
if uuid is not None:
|
||||
self.book_db_uuid_cache[i].add(uuid)
|
||||
|
||||
mi = self.library_view.model().db.get_metadata(id, index_is_id=True)
|
||||
for i, l in enumerate(self.booklists()):
|
||||
if mi.uuid in self.book_db_uuid_cache[i]:
|
||||
loc[i] = True
|
||||
continue
|
||||
db_title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
||||
cache = self.book_db_title_cache[i].get(db_title, None)
|
||||
if cache:
|
||||
if id in cache['db_ids']:
|
||||
loc[i] = True
|
||||
continue
|
||||
if mi.authors and \
|
||||
re.sub('(?u)\W|[_]', '', authors_to_string(mi.authors).lower()) \
|
||||
in cache['authors']:
|
||||
loc[i] = True
|
||||
continue
|
||||
return loc
|
||||
|
||||
def set_books_in_library(self, booklists, reset=False):
|
||||
if reset:
|
||||
# First build a cache of the library, so the search isn't On**2
|
||||
self.db_book_title_cache = {}
|
||||
self.db_book_uuid_cache = set()
|
||||
db = self.library_view.model().db
|
||||
for id in db.data.iterallids():
|
||||
mi = db.get_metadata(id, index_is_id=True)
|
||||
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
||||
if title not in self.db_book_title_cache:
|
||||
self.db_book_title_cache[title] = {'authors':{}, 'db_ids':{}}
|
||||
authors = authors_to_string(mi.authors).lower() if mi.authors else ''
|
||||
authors = re.sub('(?u)\W|[_]', '', authors)
|
||||
self.db_book_title_cache[title]['authors'][authors] = mi
|
||||
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
|
||||
self.db_book_uuid_cache.add(mi.uuid)
|
||||
|
||||
# Now iterate through all the books on the device, setting the
|
||||
# in_library field Fastest and most accurate key is the uuid. Second is
|
||||
# the application_id, which is really the db key, but as this can
|
||||
# accidentally match across libraries we also verify the title. The
|
||||
# db_id exists on Sony devices. Fallback is title and author match
|
||||
resend_metadata = False
|
||||
for booklist in booklists:
|
||||
for book in booklist:
|
||||
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
||||
book.in_library = True
|
||||
continue
|
||||
|
||||
book_title = book.title.lower() if book.title else ''
|
||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
||||
book.in_library = False
|
||||
d = self.db_book_title_cache.get(book_title, None)
|
||||
if d is not None:
|
||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||
book.in_library = True
|
||||
book.smart_update(d['db_ids'][book.application_id])
|
||||
resend_metadata = True
|
||||
continue
|
||||
if book.db_id in d['db_ids']:
|
||||
book.in_library = True
|
||||
book.smart_update(d['db_ids'][book.db_id])
|
||||
resend_metadata = True
|
||||
continue
|
||||
book_authors = authors_to_string(book.authors).lower() if book.authors else ''
|
||||
book_authors = re.sub('(?u)\W|[_]', '', book_authors)
|
||||
if book_authors in d['authors']:
|
||||
book.in_library = True
|
||||
book.smart_update(d['authors'][book_authors])
|
||||
resend_metadata = True
|
||||
# Set author_sort if it isn't already
|
||||
asort = getattr(book, 'author_sort', None)
|
||||
if not asort and book.authors:
|
||||
book.author_sort = authors_to_sort_string(book.authors)
|
||||
resend_metadata = True
|
||||
|
||||
if resend_metadata:
|
||||
# Correct the metadata cache on device.
|
||||
if self.device_manager.is_device_connected:
|
||||
self.device_manager.sync_booklists(None, booklists)
|
||||
|
@ -6,7 +6,6 @@ __docformat__ = 'restructuredtext en'
|
||||
''''''
|
||||
from PyQt4.QtGui import QDialog
|
||||
from calibre.gui2.dialogs.comicconf_ui import Ui_Dialog
|
||||
from calibre.gui2 import qstring_to_unicode
|
||||
from calibre.ebooks.lrf.comic.convert_from import config, PROFILES
|
||||
|
||||
def set_conversion_defaults(window):
|
||||
@ -78,9 +77,9 @@ class ComicConf(QDialog, Ui_Dialog):
|
||||
elif hasattr(g, 'value'):
|
||||
val = g.value()
|
||||
elif hasattr(g, 'itemText'):
|
||||
val = qstring_to_unicode(g.itemText(g.currentIndex()))
|
||||
val = unicode(g.itemText(g.currentIndex()))
|
||||
elif hasattr(g, 'text'):
|
||||
val = qstring_to_unicode(g.text())
|
||||
val = unicode(g.text())
|
||||
else:
|
||||
raise Exception('Bad coding')
|
||||
self.config.set(opt.name, val)
|
||||
|
17
src/calibre/gui2/dialogs/comments_dialog.py
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
from PyQt4.Qt import QDialog
|
||||
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
|
||||
|
||||
class CommentsDialog(QDialog, Ui_CommentsDialog):
|
||||
|
||||
def __init__(self, parent, text):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_CommentsDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
if text is not None:
|
||||
self.textbox.setPlainText(text)
|
||||
self.textbox.setTabChangesFocus(True)
|
83
src/calibre/gui2/dialogs/comments_dialog.ui
Normal file
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CommentsDialog</class>
|
||||
<widget class="QDialog" name="CommentsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>336</width>
|
||||
<height>235</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Edit Comments</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="verticalLayoutWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>311</width>
|
||||
<height>211</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="textbox"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>CommentsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>229</x>
|
||||
<y>211</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>234</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>CommentsDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>297</x>
|
||||
<y>217</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>234</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -1,6 +1,7 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import os, re, time, textwrap
|
||||
|
||||
import os, re, time, textwrap, copy, sys
|
||||
|
||||
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||
@ -12,14 +13,14 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
|
||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||
warning_dialog, ResizableDialog
|
||||
warning_dialog, ResizableDialog, question_dialog
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.gui2.library import BooksModel
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.oeb.iterator import is_supported
|
||||
from calibre.library import server_config
|
||||
from calibre.library.server import server_config
|
||||
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
||||
disable_plugin, customize_plugin, \
|
||||
plugin_customization, add_plugin, \
|
||||
@ -90,7 +91,6 @@ class ConfigTabs(QTabWidget):
|
||||
widget.commit(save_defaults=True)
|
||||
return True
|
||||
|
||||
|
||||
class PluginModel(QAbstractItemModel):
|
||||
|
||||
def __init__(self, *args):
|
||||
@ -110,6 +110,9 @@ class PluginModel(QAbstractItemModel):
|
||||
self._data[plugin.type].append(plugin)
|
||||
self.categories = sorted(self._data.keys())
|
||||
|
||||
for plugins in self._data.values():
|
||||
plugins.sort(cmp=lambda x, y: cmp(x.name.lower(), y.name.lower()))
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
@ -328,14 +331,17 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
def category_current_changed(self, n, p):
|
||||
self.stackedWidget.setCurrentIndex(n.row())
|
||||
|
||||
def __init__(self, window, db, server=None):
|
||||
ResizableDialog.__init__(self, window)
|
||||
def __init__(self, parent, library_view, server=None):
|
||||
ResizableDialog.__init__(self, parent)
|
||||
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
||||
self._category_model = CategoryModel()
|
||||
|
||||
self.category_view.currentChanged = self.category_current_changed
|
||||
self.category_view.setModel(self._category_model)
|
||||
self.db = db
|
||||
self.parent = parent
|
||||
self.library_view = library_view
|
||||
self.model = library_view.model()
|
||||
self.db = self.model.db
|
||||
self.server = server
|
||||
path = prefs['library_path']
|
||||
self.location.setText(path if path else '')
|
||||
@ -359,18 +365,28 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.roman_numerals.setChecked(rn)
|
||||
self.new_version_notification.setChecked(config['new_version_notification'])
|
||||
|
||||
column_map = config['column_map']
|
||||
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
|
||||
try:
|
||||
item = QListWidgetItem(BooksModel.headers[col], self.columns)
|
||||
except KeyError:
|
||||
continue
|
||||
# Set up columns
|
||||
colmap = list(self.model.column_map)
|
||||
state = self.library_view.get_state()
|
||||
hidden_cols = state['hidden_columns']
|
||||
positions = state['column_positions']
|
||||
colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y]))
|
||||
self.custcols = copy.deepcopy(self.db.field_metadata.get_custom_field_metadata())
|
||||
for col in colmap:
|
||||
item = QListWidgetItem(self.model.headers[col], self.columns)
|
||||
item.setData(Qt.UserRole, QVariant(col))
|
||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked)
|
||||
|
||||
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
||||
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
||||
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
|
||||
if col != 'ondevice':
|
||||
flags |= Qt.ItemIsUserCheckable
|
||||
item.setFlags(flags)
|
||||
if col != 'ondevice':
|
||||
item.setCheckState(Qt.Unchecked if col in hidden_cols else
|
||||
Qt.Checked)
|
||||
self.column_up.clicked.connect(self.up_column)
|
||||
self.column_down.clicked.connect(self.down_column)
|
||||
self.del_custcol_button.clicked.connect(self.del_custcol)
|
||||
self.add_custcol_button.clicked.connect(self.add_custcol)
|
||||
self.edit_custcol_button.clicked.connect(self.edit_custcol)
|
||||
|
||||
icons = config['toolbar_icon_size']
|
||||
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
||||
@ -401,7 +417,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
for item in items:
|
||||
self.language.addItem(item[1], QVariant(item[0]))
|
||||
|
||||
|
||||
exts = set([])
|
||||
for ext in BOOK_EXTENSIONS:
|
||||
ext = ext.lower()
|
||||
@ -465,6 +480,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
|
||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||
self.port.editingFinished.connect(self.check_port_value)
|
||||
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
||||
True))
|
||||
|
||||
def check_port_value(self, *args):
|
||||
port = self.port.value()
|
||||
@ -635,6 +652,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.input_order.insertItem(idx+1, self.input_order.takeItem(idx))
|
||||
self.input_order.setCurrentRow(idx+1)
|
||||
|
||||
# Column settings {{{
|
||||
def up_column(self):
|
||||
idx = self.columns.currentRow()
|
||||
if idx > 0:
|
||||
@ -647,6 +665,77 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
|
||||
self.columns.setCurrentRow(idx+1)
|
||||
|
||||
def del_custcol(self):
|
||||
idx = self.columns.currentRow()
|
||||
if idx < 0:
|
||||
return error_dialog(self, '', _('You must select a column to delete it'),
|
||||
show=True)
|
||||
col = unicode(self.columns.item(idx).data(Qt.UserRole).toString())
|
||||
if col not in self.custcols:
|
||||
return error_dialog(self, '',
|
||||
_('The selected column is not a custom column'), show=True)
|
||||
if not question_dialog(self, _('Are you sure?'),
|
||||
_('Do you really want to delete column %s and all its data?') %
|
||||
self.custcols[col]['name'], show_copy_button=False):
|
||||
return
|
||||
self.columns.item(idx).setCheckState(False)
|
||||
self.columns.takeItem(idx)
|
||||
self.custcols[col]['*deleteme'] = True
|
||||
return
|
||||
|
||||
def add_custcol(self):
|
||||
CreateCustomColumn(self, False, self.model.orig_headers, ALL_COLUMNS)
|
||||
|
||||
def edit_custcol(self):
|
||||
CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS)
|
||||
|
||||
def apply_custom_column_changes(self):
|
||||
config_cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||
for i in range(self.columns.count())]
|
||||
if not config_cols:
|
||||
config_cols = ['title']
|
||||
removed_cols = set(self.model.column_map) - set(config_cols)
|
||||
hidden_cols = set([unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||
for i in range(self.columns.count()) \
|
||||
if self.columns.item(i).checkState()==Qt.Unchecked])
|
||||
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
|
||||
hidden_cols = list(hidden_cols.intersection(set(self.model.column_map)))
|
||||
if 'ondevice' in hidden_cols:
|
||||
hidden_cols.remove('ondevice')
|
||||
def col_pos(x, y):
|
||||
xidx = config_cols.index(x) if x in config_cols else sys.maxint
|
||||
yidx = config_cols.index(y) if y in config_cols else sys.maxint
|
||||
return cmp(xidx, yidx)
|
||||
positions = {}
|
||||
for i, col in enumerate((sorted(self.model.column_map, cmp=col_pos))):
|
||||
positions[col] = i
|
||||
state = {'hidden_columns': hidden_cols, 'column_positions':positions}
|
||||
self.library_view.apply_state(state)
|
||||
self.library_view.save_state()
|
||||
|
||||
must_restart = False
|
||||
for c in self.custcols:
|
||||
if self.custcols[c]['colnum'] is None:
|
||||
self.db.create_custom_column(
|
||||
label=self.custcols[c]['label'],
|
||||
name=self.custcols[c]['name'],
|
||||
datatype=self.custcols[c]['datatype'],
|
||||
is_multiple=self.custcols[c]['is_multiple'],
|
||||
display = self.custcols[c]['display'])
|
||||
must_restart = True
|
||||
elif '*deleteme' in self.custcols[c]:
|
||||
self.db.delete_custom_column(label=self.custcols[c]['label'])
|
||||
must_restart = True
|
||||
elif '*edited' in self.custcols[c]:
|
||||
cc = self.custcols[c]
|
||||
self.db.set_custom_column_metadata(cc['colnum'], name=cc['name'],
|
||||
label=cc['label'],
|
||||
display = self.custcols[c]['display'])
|
||||
if '*must_restart' in self.custcols[c]:
|
||||
must_restart = True
|
||||
return must_restart
|
||||
# }}}
|
||||
|
||||
def view_server_logs(self):
|
||||
from calibre.library.server import log_access_file, log_error_file
|
||||
d = QDialog(self)
|
||||
@ -683,7 +772,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
|
||||
def start_server(self):
|
||||
self.set_server_options()
|
||||
from calibre.library.server import start_threaded_server
|
||||
from calibre.library.server.main import start_threaded_server
|
||||
self.server = start_threaded_server(self.db, server_config().parse())
|
||||
while not self.server.is_running and self.server.exception is None:
|
||||
time.sleep(1)
|
||||
@ -696,7 +785,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
self.stop.setEnabled(True)
|
||||
|
||||
def stop_server(self):
|
||||
from calibre.library.server import stop_threaded_server
|
||||
from calibre.library.server.main import stop_threaded_server
|
||||
stop_threaded_server(self.server)
|
||||
self.server = None
|
||||
self.start.setEnabled(True)
|
||||
@ -716,7 +805,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
if dir:
|
||||
self.location.setText(dir)
|
||||
|
||||
|
||||
def accept(self):
|
||||
mcs = unicode(self.max_cover_size.text()).strip()
|
||||
if not re.match(r'\d+x\d+', mcs):
|
||||
@ -734,17 +822,15 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
wl += 1
|
||||
config['worker_limit'] = wl
|
||||
|
||||
|
||||
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
||||
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
||||
prefs['network_timeout'] = int(self.timeout.value())
|
||||
path = qstring_to_unicode(self.location.text())
|
||||
path = unicode(self.location.text())
|
||||
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
||||
prefs['input_format_order'] = input_cols
|
||||
cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked]
|
||||
if not cols:
|
||||
cols = ['title']
|
||||
config['column_map'] = cols
|
||||
|
||||
must_restart = self.apply_custom_column_changes()
|
||||
|
||||
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
||||
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
||||
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
|
||||
@ -768,6 +854,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
|
||||
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
||||
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
||||
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
|
||||
fmts = []
|
||||
for i in range(self.viewer.count()):
|
||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||
@ -785,6 +872,12 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
||||
d.exec_()
|
||||
else:
|
||||
self.database_location = os.path.abspath(path)
|
||||
if must_restart:
|
||||
warning_dialog(self, _('Must restart'),
|
||||
_('The changes you made require that Calibre be '
|
||||
'restarted. Please restart as soon as practical.'),
|
||||
show=True, show_copy_button=False)
|
||||
self.parent.must_restart_before_config = True
|
||||
QDialog.accept(self)
|
||||
|
||||
class VacThread(QThread):
|
||||
|
@ -331,8 +331,8 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_8">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="roman_numerals">
|
||||
<property name="text">
|
||||
<string>Use &Roman numerals for series number</string>
|
||||
@ -342,28 +342,35 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="systray_icon">
|
||||
<property name="text">
|
||||
<string>Enable system &tray icon (needs restart)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="systray_notifications">
|
||||
<property name="text">
|
||||
<string>Show &notifications in system tray</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="show_splash_screen">
|
||||
<property name="text">
|
||||
<string>Show &splash screen at startup</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="separate_cover_flow">
|
||||
<property name="text">
|
||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="search_as_you_type">
|
||||
<property name="text">
|
||||
<string>Search as you type</string>
|
||||
@ -373,21 +380,21 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="sync_news">
|
||||
<property name="text">
|
||||
<string>Automatically send downloaded &news to ebook reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="delete_news">
|
||||
<property name="text">
|
||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
@ -404,7 +411,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Toolbar</string>
|
||||
@ -452,7 +459,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="9" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
@ -498,6 +505,87 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="del_custcol_button">
|
||||
<property name="toolTip">
|
||||
<string>Remove a user-defined column</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../../resources/images.qrc">
|
||||
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="add_custcol_button">
|
||||
<property name="toolTip">
|
||||
<string>Add a user-defined column</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../../resources/images.qrc">
|
||||
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="edit_custcol_button">
|
||||
<property name="toolTip">
|
||||
<string>Edit settings of a user-defined column</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../../resources/images.qrc">
|
||||
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="column_down">
|
||||
<property name="text">
|
||||
|
164
src/calibre/gui2/dialogs/config/create_custom_column.py
Normal file
@ -0,0 +1,164 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
'''Dialog to create a new custom column'''
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
|
||||
|
||||
from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn
|
||||
from calibre.gui2 import error_dialog
|
||||
|
||||
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
|
||||
column_types = {
|
||||
0:{'datatype':'text',
|
||||
'text':_('Text, column shown in the tag browser'),
|
||||
'is_multiple':False},
|
||||
1:{'datatype':'*text',
|
||||
'text':_('Comma separated text, like tags, shown in the tag browser'),
|
||||
'is_multiple':True},
|
||||
2:{'datatype':'comments',
|
||||
'text':_('Long text, like comments, not shown in the tag browser'),
|
||||
'is_multiple':False},
|
||||
3:{'datatype':'datetime',
|
||||
'text':_('Date'), 'is_multiple':False},
|
||||
4:{'datatype':'float',
|
||||
'text':_('Floating point numbers'), 'is_multiple':False},
|
||||
5:{'datatype':'int',
|
||||
'text':_('Integers'), 'is_multiple':False},
|
||||
6:{'datatype':'rating',
|
||||
'text':_('Ratings, shown with stars'),
|
||||
'is_multiple':False},
|
||||
7:{'datatype':'bool',
|
||||
'text':_('Yes/No'), 'is_multiple':False},
|
||||
}
|
||||
|
||||
def __init__(self, parent, editing, standard_colheads, standard_colnames):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_QCreateCustomColumn.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.simple_error = partial(error_dialog, self, show=True,
|
||||
show_copy_button=False)
|
||||
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
|
||||
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
|
||||
self.parent = parent
|
||||
self.editing_col = editing
|
||||
self.standard_colheads = standard_colheads
|
||||
self.standard_colnames = standard_colnames
|
||||
for t in self.column_types:
|
||||
self.column_type_box.addItem(self.column_types[t]['text'])
|
||||
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
|
||||
if not self.editing_col:
|
||||
self.datatype_changed()
|
||||
self.exec_()
|
||||
return
|
||||
idx = parent.columns.currentRow()
|
||||
if idx < 0:
|
||||
self.simple_error(_('No column selected'),
|
||||
_('No column has been selected'))
|
||||
return
|
||||
col = unicode(parent.columns.item(idx).data(Qt.UserRole).toString())
|
||||
if col not in parent.custcols:
|
||||
self.simple_error('', _('Selected column is not a user-defined column'))
|
||||
return
|
||||
|
||||
c = parent.custcols[col]
|
||||
self.column_name_box.setText(c['label'])
|
||||
self.column_heading_box.setText(c['name'])
|
||||
ct = c['datatype'] if not c['is_multiple'] else '*text'
|
||||
self.orig_column_number = c['colnum']
|
||||
self.orig_column_name = col
|
||||
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
|
||||
self.column_type_box.setCurrentIndex(column_numbers[ct])
|
||||
self.column_type_box.setEnabled(False)
|
||||
if ct == 'datetime':
|
||||
if c['display'].get('date_format', None):
|
||||
self.date_format_box.setText(c['display'].get('date_format', ''))
|
||||
self.datatype_changed()
|
||||
self.exec_()
|
||||
|
||||
def datatype_changed(self, *args):
|
||||
try:
|
||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
||||
except:
|
||||
col_type = None
|
||||
df_visible = col_type == 'datetime'
|
||||
for x in ('box', 'default_label', 'label'):
|
||||
getattr(self, 'date_format_'+x).setVisible(df_visible)
|
||||
|
||||
|
||||
def accept(self):
|
||||
col = unicode(self.column_name_box.text()).lower()
|
||||
if not col:
|
||||
return self.simple_error('', _('No lookup name was provided'))
|
||||
if not col.isalnum() or not col[0].isalpha():
|
||||
return self.simple_error('', _('The label must contain only letters and digits, and start with a letter'))
|
||||
col_heading = unicode(self.column_heading_box.text())
|
||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
||||
if col_type == '*text':
|
||||
col_type='text'
|
||||
is_multiple = True
|
||||
else:
|
||||
is_multiple = False
|
||||
if not col_heading:
|
||||
return self.simple_error('', _('No column heading was provided'))
|
||||
bad_col = False
|
||||
if col in self.parent.custcols:
|
||||
if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number:
|
||||
bad_col = True
|
||||
if bad_col:
|
||||
return self.simple_error('', _('The lookup name %s is already used')%col)
|
||||
bad_head = False
|
||||
for t in self.parent.custcols:
|
||||
if self.parent.custcols[t]['name'] == col_heading:
|
||||
if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number:
|
||||
bad_head = True
|
||||
for t in self.standard_colheads:
|
||||
if self.standard_colheads[t] == col_heading:
|
||||
bad_head = True
|
||||
if bad_head:
|
||||
return self.simple_error('', _('The heading %s is already used')%col_heading)
|
||||
if ':' in col or ' ' in col or col.lower() != col:
|
||||
return self.simple_error('', _('The lookup name must be lower case and cannot contain ":"s or spaces'))
|
||||
|
||||
date_format = {}
|
||||
if col_type == 'datetime':
|
||||
if self.date_format_box.text():
|
||||
date_format = {'date_format':unicode(self.date_format_box.text())}
|
||||
else:
|
||||
date_format = {'date_format': None}
|
||||
|
||||
key = self.parent.db.field_metadata.custom_field_prefix+col
|
||||
if not self.editing_col:
|
||||
self.parent.db.field_metadata
|
||||
self.parent.custcols[key] = {
|
||||
'label':col,
|
||||
'name':col_heading,
|
||||
'datatype':col_type,
|
||||
'editable':True,
|
||||
'display':date_format,
|
||||
'normalized':None,
|
||||
'colnum':None,
|
||||
'is_multiple':is_multiple,
|
||||
}
|
||||
item = QListWidgetItem(col_heading, self.parent.columns)
|
||||
item.setData(Qt.UserRole, QVariant(key))
|
||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||
item.setCheckState(Qt.Checked)
|
||||
else:
|
||||
idx = self.parent.columns.currentRow()
|
||||
item = self.parent.columns.item(idx)
|
||||
item.setData(Qt.UserRole, QVariant(key))
|
||||
item.setText(col_heading)
|
||||
self.parent.custcols[self.orig_column_name]['label'] = col
|
||||
self.parent.custcols[self.orig_column_name]['name'] = col_heading
|
||||
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
|
||||
self.parent.custcols[self.orig_column_name]['*edited'] = True
|
||||
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
QDialog.reject(self)
|
191
src/calibre/gui2/dialogs/config/create_custom_column.ui
Normal file
@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>QCreateCustomColumn</class>
|
||||
<widget class="QDialog" name="QCreateCustomColumn">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>528</width>
|
||||
<height>199</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Create or edit custom columns</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="2" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Lookup name</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>column_name_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Column &heading</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>column_heading_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="column_name_box">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Used for searching the column. Must contain only digits and lower case letters.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="column_heading_box">
|
||||
<property name="toolTip">
|
||||
<string>Column heading in the library view and category name in the tag browser</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Column &type</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>column_type_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="column_type_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>What kind of information will be kept in the column.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="date_format_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p>
|
||||
<p>For example:
|
||||
<ul>
|
||||
<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li>
|
||||
<li>dd MMMM yy gives 05 January 10</li>
|
||||
</ul> </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="date_format_default_label">
|
||||
<property name="toolTip">
|
||||
<string>Use MMM yyyy for month + year, yyyy for year only</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Default: dd MMM yyyy.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="date_format_label">
|
||||
<property name="text">
|
||||
<string>Format for &dates</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>date_format_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Create or edit custom columns</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>column_name_box</tabstop>
|
||||
<tabstop>column_heading_box</tabstop>
|
||||
<tabstop>column_type_box</tabstop>
|
||||
<tabstop>date_format_box</tabstop>
|
||||
<tabstop>button_box</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -14,12 +14,12 @@
|
||||
<string>Active Jobs</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../work/calibre/resources/images.qrc">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/jobs.svg</normaloff>:/images/jobs.svg</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="JobsView" name="jobs_view">
|
||||
<widget class="QTableView" name="jobs_view">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::NoContextMenu</enum>
|
||||
</property>
|
||||
@ -66,15 +66,8 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>JobsView</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>widgets.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../work/calibre/resources/images.qrc"/>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -4,12 +4,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''Dialog to edit metadata in bulk'''
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, QObject
|
||||
from PyQt4.QtGui import QDialog
|
||||
from PyQt4.QtGui import QDialog, QGridLayout
|
||||
|
||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
|
||||
authors_to_string
|
||||
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
|
||||
|
||||
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
|
||||
@ -19,7 +20,8 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
self.setupUi(self)
|
||||
self.db = db
|
||||
self.ids = [db.id(r) for r in rows]
|
||||
self.groupBox.setTitle(_('Editing meta information for %d books') %
|
||||
self.box_title.setText('<p>' +
|
||||
_('Editing meta information for <b>%d books</b>') %
|
||||
len(rows))
|
||||
self.write_series = False
|
||||
self.changed = False
|
||||
@ -38,9 +40,27 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.series_changed)
|
||||
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.series_changed)
|
||||
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), self.tag_editor)
|
||||
if len(db.custom_column_label_map) == 0:
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
else:
|
||||
self.create_custom_column_editors()
|
||||
|
||||
self.exec_()
|
||||
|
||||
def create_custom_column_editors(self):
|
||||
w = self.central_widget.widget(1)
|
||||
layout = QGridLayout()
|
||||
|
||||
self.custom_column_widgets, self.__cc_spacers = populate_bulk_metadata_page(
|
||||
layout, self.db, self.ids, w)
|
||||
w.setLayout(layout)
|
||||
self.__custom_col_layouts = [layout]
|
||||
ans = self.custom_column_widgets
|
||||
for i in range(len(ans)-1):
|
||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||
for c in range(2, len(ans[i].widgets), 2):
|
||||
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||
|
||||
def initialize_combos(self):
|
||||
self.initalize_authors()
|
||||
self.initialize_series()
|
||||
@ -133,7 +153,13 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
new_authors = string_to_authors(title)
|
||||
self.db.set_authors(id, new_authors, notify=False)
|
||||
|
||||
if self.remove_conversion_settings.isChecked():
|
||||
self.db.delete_conversion_options(id, 'PIPE')
|
||||
|
||||
self.changed = True
|
||||
for w in getattr(self, 'custom_column_widgets', []):
|
||||
w.commit(self.ids)
|
||||
|
||||
|
||||
def series_changed(self):
|
||||
self.write_series = True
|
||||
|
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>495</width>
|
||||
<height>468</height>
|
||||
<width>526</width>
|
||||
<height>499</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -18,6 +18,16 @@
|
||||
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="box_title">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout">
|
||||
<property name="spacing">
|
||||
@ -27,10 +37,14 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Meta information</string>
|
||||
<widget class="QTabWidget" name="central_widget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tabWidgetPage1">
|
||||
<attribute name="title">
|
||||
<string>&Basic metadata</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
@ -239,7 +253,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="2">
|
||||
<item row="11" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="swap_title_and_author">
|
||||
<property name="text">
|
||||
<string>&Swap title and author</string>
|
||||
@ -259,8 +273,26 @@ Book A will have series number 1 and Book B series number 2.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="remove_conversion_settings">
|
||||
<property name="toolTip">
|
||||
<string>Remove stored conversion settings for the selected books.
|
||||
|
||||
Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove &stored conversion settings for the selected books</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>&Custom metadata</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
@ -344,21 +376,5 @@ Book A will have series number 1 and Book B series number 2.</string>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>auto_author_sort</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>author_sort</receiver>
|
||||
<slot>setDisabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>240</x>
|
||||
<y>95</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>240</x>
|
||||
<y>113</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
@ -11,10 +11,11 @@ import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import sip
|
||||
from PyQt4.Qt import SIGNAL, QObject, QCoreApplication, Qt, QTimer, QThread, QDate, \
|
||||
QPixmap, QListWidgetItem, QDialog
|
||||
QPixmap, QListWidgetItem, QDialog, QHBoxLayout, QGridLayout
|
||||
|
||||
from calibre.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
|
||||
from calibre.gui2 import error_dialog, file_icon_provider, \
|
||||
choose_files, choose_images, ResizableDialog, \
|
||||
warning_dialog
|
||||
from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
|
||||
@ -31,6 +32,7 @@ from calibre.utils.config import prefs, tweaks
|
||||
from calibre.utils.date import qt_to_dt
|
||||
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
|
||||
from calibre.gui2.dialogs.config.social import SocialMetadata
|
||||
from calibre.gui2.custom_column_widgets import populate_single_metadata_page
|
||||
|
||||
class CoverFetcher(QThread):
|
||||
|
||||
@ -311,6 +313,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.cpixmap = None
|
||||
self.cover.setAcceptDrops(True)
|
||||
self.pubdate.setMinimumDate(QDate(100,1,1))
|
||||
pubdate_format = tweaks['gui_pubdate_display_format']
|
||||
if pubdate_format is not None:
|
||||
self.pubdate.setDisplayFormat(pubdate_format)
|
||||
self.date.setMinimumDate(QDate(100,1,1))
|
||||
|
||||
self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped)
|
||||
@ -405,6 +410,30 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.cover.setPixmap(pm)
|
||||
self.cover_data = cover
|
||||
self.original_series_name = unicode(self.series.text()).strip()
|
||||
if len(db.custom_column_label_map) == 0:
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
else:
|
||||
self.create_custom_column_editors()
|
||||
|
||||
def create_custom_column_editors(self):
|
||||
w = self.central_widget.widget(1)
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setSpacing(20)
|
||||
left_layout = QGridLayout()
|
||||
right_layout = QGridLayout()
|
||||
top_layout.addLayout(left_layout)
|
||||
|
||||
self.custom_column_widgets, self.__cc_spacers = populate_single_metadata_page(
|
||||
left_layout, right_layout, self.db, self.id, w)
|
||||
top_layout.addLayout(right_layout)
|
||||
sip.delete(w.layout())
|
||||
w.setLayout(top_layout)
|
||||
self.__custom_col_layouts = [top_layout, left_layout, right_layout]
|
||||
ans = self.custom_column_widgets
|
||||
for i in range(len(ans)-1):
|
||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[-1])
|
||||
|
||||
|
||||
|
||||
def validate_isbn(self, isbn):
|
||||
isbn = unicode(isbn).strip()
|
||||
@ -559,12 +588,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
def fetch_metadata(self):
|
||||
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text()))
|
||||
title = qstring_to_unicode(self.title.text())
|
||||
title = unicode(self.title.text())
|
||||
try:
|
||||
author = string_to_authors(unicode(self.authors.text()))[0]
|
||||
except:
|
||||
author = ''
|
||||
publisher = qstring_to_unicode(self.publisher.currentText())
|
||||
publisher = unicode(self.publisher.currentText())
|
||||
if isbn or title or author or publisher:
|
||||
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
|
||||
self._fetch_metadata_scope = d
|
||||
@ -630,12 +659,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
|
||||
def remove_unused_series(self):
|
||||
self.db.remove_unused_series()
|
||||
idx = qstring_to_unicode(self.series.currentText())
|
||||
idx = unicode(self.series.currentText())
|
||||
self.series.clear()
|
||||
self.initialize_series()
|
||||
if idx:
|
||||
for i in range(self.series.count()):
|
||||
if qstring_to_unicode(self.series.itemText(i)) == idx:
|
||||
if unicode(self.series.itemText(i)) == idx:
|
||||
self.series.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
@ -655,7 +684,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.db.set_isbn(self.id,
|
||||
re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())), notify=False)
|
||||
self.db.set_rating(self.id, 2*self.rating.value(), notify=False)
|
||||
self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.currentText()), notify=False)
|
||||
self.db.set_publisher(self.id, unicode(self.publisher.currentText()), notify=False)
|
||||
self.db.set_tags(self.id, [x.strip() for x in
|
||||
unicode(self.tags.text()).split(',')], notify=False)
|
||||
self.db.set_series(self.id,
|
||||
@ -675,6 +704,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.db.set_cover(self.id, self.cover_data)
|
||||
else:
|
||||
self.db.remove_cover(self.id)
|
||||
for w in getattr(self, 'custom_column_widgets', []):
|
||||
w.commit(self.id)
|
||||
except IOError, err:
|
||||
if err.errno == 13: # Permission denied
|
||||
fname = err.filename if err.filename else 'file'
|
||||
|
@ -43,8 +43,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>869</width>
|
||||
<height>698</height>
|
||||
<width>879</width>
|
||||
<height>711</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
@ -52,13 +52,20 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="central_widget" native="true">
|
||||
<widget class="QTabWidget" name="central_widget">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>800</width>
|
||||
<height>665</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="central_tabWidgetPage1">
|
||||
<attribute name="title">
|
||||
<string>&Basic metadata</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
@ -409,6 +416,9 @@
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextEdit" name="comments">
|
||||
<property name="tabChangesFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="acceptRichText">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
@ -672,6 +682,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>&Custom metadata</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2"/>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
@ -5,7 +5,7 @@ from PyQt4.QtGui import QDialog, QLineEdit
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
|
||||
from calibre.gui2.dialogs.password_ui import Ui_Dialog
|
||||
from calibre.gui2 import qstring_to_unicode, dynamic
|
||||
from calibre.gui2 import dynamic
|
||||
|
||||
class PasswordDialog(QDialog, Ui_Dialog):
|
||||
|
||||
@ -32,10 +32,10 @@ class PasswordDialog(QDialog, Ui_Dialog):
|
||||
self.gui_password.setEchoMode(QLineEdit.Normal)
|
||||
|
||||
def username(self):
|
||||
return qstring_to_unicode(self.gui_username.text())
|
||||
return unicode(self.gui_username.text())
|
||||
|
||||
def password(self):
|
||||
return qstring_to_unicode(self.gui_password.text())
|
||||
return unicode(self.gui_password.text())
|
||||
|
||||
def accept(self):
|
||||
dynamic.set(self.cfg_key+'__un', unicode(self.gui_username.text()))
|
||||
|
84
src/calibre/gui2/dialogs/saved_search_editor.py
Normal file
@ -0,0 +1,84 @@
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
|
||||
from PyQt4.QtCore import SIGNAL
|
||||
from PyQt4.QtGui import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
|
||||
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
|
||||
|
||||
def __init__(self, window, initial_search=None):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_SavedSearchEditor.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.connect(self.add_search_button, SIGNAL('clicked()'), self.add_search)
|
||||
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
|
||||
self.current_index_changed)
|
||||
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
|
||||
|
||||
self.current_search_name = None
|
||||
self.searches = {}
|
||||
self.searches_to_delete = []
|
||||
for name in saved_searches.names():
|
||||
self.searches[name] = saved_searches.lookup(name)
|
||||
|
||||
self.populate_search_list()
|
||||
if initial_search is not None and initial_search in self.searches:
|
||||
self.select_search(initial_search)
|
||||
|
||||
def populate_search_list(self):
|
||||
self.search_name_box.clear()
|
||||
for name in sorted(self.searches.keys()):
|
||||
self.search_name_box.addItem(name)
|
||||
|
||||
def add_search(self):
|
||||
search_name = unicode(self.input_box.text()).strip()
|
||||
if search_name == '':
|
||||
return False
|
||||
if search_name not in self.searches:
|
||||
self.searches[search_name] = ''
|
||||
self.populate_search_list()
|
||||
self.select_search(search_name)
|
||||
else:
|
||||
self.select_search(search_name)
|
||||
return True
|
||||
|
||||
def del_search(self):
|
||||
if self.current_search_name is not None:
|
||||
if not confirm('<p>'+_('The current saved search will be '
|
||||
'<b>permanently deleted</b>. Are you sure?')
|
||||
+'</p>', 'saved_search_editor_delete', self):
|
||||
return
|
||||
del self.searches[self.current_search_name]
|
||||
self.searches_to_delete.append(self.current_search_name)
|
||||
self.current_search_name = None
|
||||
self.search_name_box.removeItem(self.search_name_box.currentIndex())
|
||||
|
||||
def select_search(self, name):
|
||||
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
|
||||
|
||||
def current_index_changed(self, idx):
|
||||
if self.current_search_name:
|
||||
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
|
||||
name = unicode(self.search_name_box.itemText(idx))
|
||||
if name:
|
||||
self.current_search_name = name
|
||||
self.search_text.setPlainText(self.searches[name])
|
||||
else:
|
||||
self.current_search_name = None
|
||||
self.search_text.setPlainText('')
|
||||
|
||||
def accept(self):
|
||||
if self.current_search_name:
|
||||
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
|
||||
for name in self.searches_to_delete:
|
||||
saved_searches.delete(name)
|
||||
for name in self.searches:
|
||||
saved_searches.add(name, self.searches[name])
|
||||
QDialog.accept(self)
|
185
src/calibre/gui2/dialogs/saved_search_editor.ui
Normal file
@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SavedSearchEditor</class>
|
||||
<widget class="QDialog" name="SavedSearchEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>548</width>
|
||||
<height>148</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Saved Search Editor</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Saved Search: </string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>search_name_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="search_name_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>160</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>145</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Select a saved search to edit</string>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QToolButton" name="delete_search_button">
|
||||
<property name="toolTip">
|
||||
<string>Delete this selected saved search</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QLineEdit" name="input_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>60</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Enter a new saved search name.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="5">
|
||||
<widget class="QToolButton" name="add_search_button">
|
||||
<property name="toolTip">
|
||||
<string>Add the new saved search</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPlainTextEdit" name="search_text">
|
||||
<property name="toolTip">
|
||||
<string>Change the contents of the saved search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../../calibre_datesearch/resources/images"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SavedSearchEditor</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SavedSearchEditor</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -32,8 +32,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
self.search.setMinimumContentsLength(25)
|
||||
self.search.initialize('scheduler_search_history')
|
||||
self.recipe_box.layout().insertWidget(0, self.search)
|
||||
self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'),
|
||||
self.recipe_model.search)
|
||||
self.search.search.connect(self.recipe_model.search)
|
||||
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
||||
self.search.search_done)
|
||||
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
||||
|
@ -4,7 +4,6 @@ import re
|
||||
from PyQt4.QtGui import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.search_ui import Ui_Dialog
|
||||
from calibre.gui2 import qstring_to_unicode
|
||||
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||
|
||||
class SearchDialog(QDialog, Ui_Dialog):
|
||||
@ -48,11 +47,11 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
return ans
|
||||
|
||||
def token(self):
|
||||
txt = qstring_to_unicode(self.text.text()).strip()
|
||||
txt = unicode(self.text.text()).strip()
|
||||
if txt:
|
||||
if self.negate.isChecked():
|
||||
txt = '!'+txt
|
||||
tok = self.FIELDS[qstring_to_unicode(self.field.currentText())]+txt
|
||||
tok = self.FIELDS[unicode(self.field.currentText())]+txt
|
||||
if re.search(r'\s', tok):
|
||||
tok = '"%s"'%tok
|
||||
return tok
|
||||
|
198
src/calibre/gui2/dialogs/tag_categories.py
Normal file
@ -0,0 +1,198 @@
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||
|
||||
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.constants import islinux
|
||||
|
||||
class Item:
|
||||
def __init__(self, name, label, index, icon, exists):
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.index = index
|
||||
self.icon = icon
|
||||
self.exists = exists
|
||||
def __str__(self):
|
||||
return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists)
|
||||
|
||||
class TagCategories(QDialog, Ui_TagCategories):
|
||||
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
|
||||
|
||||
def __init__(self, window, db, on_category=None):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagCategories.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.db = db
|
||||
self.applied_items = []
|
||||
|
||||
cc_icon = QIcon(I('column.svg'))
|
||||
|
||||
self.category_labels = self.category_labels_orig[:]
|
||||
category_icons = [None, QIcon(I('user_profile.svg')), QIcon(I('series.svg')),
|
||||
QIcon(I('publisher.png')), QIcon(I('tags.svg'))]
|
||||
category_values = [None,
|
||||
lambda: [n.replace('|', ',') for (id, n) in self.db.all_authors()],
|
||||
lambda: [n for (id, n) in self.db.all_series()],
|
||||
lambda: [n for (id, n) in self.db.all_publishers()],
|
||||
lambda: self.db.all_tags()
|
||||
]
|
||||
category_names = ['', _('Authors'), _('Series'), _('Publishers'), _('Tags')]
|
||||
|
||||
cc_map = self.db.custom_column_label_map
|
||||
for cc in cc_map:
|
||||
if cc_map[cc]['datatype'] == 'text':
|
||||
self.category_labels.append(db.field_metadata.label_to_key(cc))
|
||||
category_icons.append(cc_icon)
|
||||
category_values.append(lambda col=cc: self.db.all_custom(label=col))
|
||||
category_names.append(cc_map[cc]['name'])
|
||||
|
||||
self.all_items = []
|
||||
self.all_items_dict = {}
|
||||
for idx,label in enumerate(self.category_labels):
|
||||
if idx == 0:
|
||||
continue
|
||||
for n in category_values[idx]():
|
||||
t = Item(name=n, label=label, index=len(self.all_items),icon=category_icons[idx], exists=True)
|
||||
self.all_items.append(t)
|
||||
self.all_items_dict[label+':'+n] = t
|
||||
|
||||
self.categories = dict.copy(prefs['user_categories'])
|
||||
if self.categories is None:
|
||||
self.categories = {}
|
||||
for cat in self.categories:
|
||||
for item,l in enumerate(self.categories[cat]):
|
||||
key = ':'.join([l[1], l[0]])
|
||||
t = self.all_items_dict.get(key, None)
|
||||
if l[1] in self.category_labels:
|
||||
if t is None:
|
||||
t = Item(name=l[0], label=l[1], index=len(self.all_items),
|
||||
icon=category_icons[self.category_labels.index(l[1])], exists=False)
|
||||
self.all_items.append(t)
|
||||
self.all_items_dict[key] = t
|
||||
l[2] = t.index
|
||||
else:
|
||||
# remove any references to a category that no longer exists
|
||||
del self.categories[cat][item]
|
||||
|
||||
self.all_items_sorted = sorted(self.all_items, cmp=lambda x,y: cmp(x.name.lower(), y.name.lower()))
|
||||
self.display_filtered_categories(0)
|
||||
|
||||
for v in category_names:
|
||||
self.category_filter_box.addItem(v)
|
||||
self.current_cat_name = None
|
||||
|
||||
self.connect(self.apply_button, SIGNAL('clicked()'), self.apply_tags)
|
||||
self.connect(self.unapply_button, SIGNAL('clicked()'), self.unapply_tags)
|
||||
self.connect(self.add_category_button, SIGNAL('clicked()'), self.add_category)
|
||||
self.connect(self.category_box, SIGNAL('currentIndexChanged(int)'), self.select_category)
|
||||
self.connect(self.category_filter_box, SIGNAL('currentIndexChanged(int)'), self.display_filtered_categories)
|
||||
self.connect(self.delete_category_button, SIGNAL('clicked()'), self.del_category)
|
||||
if islinux:
|
||||
self.available_items_box.itemDoubleClicked.connect(self.apply_tags)
|
||||
else:
|
||||
self.connect(self.available_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags)
|
||||
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
|
||||
|
||||
self.populate_category_list()
|
||||
if on_category is not None:
|
||||
l = self.category_box.findText(on_category)
|
||||
if l >= 0:
|
||||
self.category_box.setCurrentIndex(l)
|
||||
|
||||
def make_list_widget(self, item):
|
||||
n = item.name if item.exists else item.name + _(' (not on any book)')
|
||||
w = QListWidgetItem(item.icon, n)
|
||||
w.setData(Qt.UserRole, item.index)
|
||||
return w
|
||||
|
||||
def display_filtered_categories(self, idx):
|
||||
idx = idx if idx is not None else self.category_filter_box.currentIndex()
|
||||
self.available_items_box.clear()
|
||||
self.applied_items_box.clear()
|
||||
for item in self.all_items_sorted:
|
||||
if idx == 0 or item.label == self.category_labels[idx]:
|
||||
if item.index not in self.applied_items and item.exists:
|
||||
self.available_items_box.addItem(self.make_list_widget(item))
|
||||
for index in self.applied_items:
|
||||
self.applied_items_box.addItem(self.make_list_widget(self.all_items[index]))
|
||||
|
||||
def apply_tags(self, node=None):
|
||||
if self.current_cat_name is None:
|
||||
return
|
||||
nodes = self.available_items_box.selectedItems() if node is None else [node]
|
||||
for node in nodes:
|
||||
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
|
||||
if index not in self.applied_items:
|
||||
self.applied_items.append(index)
|
||||
self.applied_items.sort(cmp=lambda x, y:cmp(self.all_items[x].name.lower(), self.all_items[y].name.lower()))
|
||||
self.display_filtered_categories(None)
|
||||
|
||||
def unapply_tags(self, node=None):
|
||||
nodes = self.applied_items_box.selectedItems() if node is None else [node]
|
||||
for node in nodes:
|
||||
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
|
||||
self.applied_items.remove(index)
|
||||
self.display_filtered_categories(None)
|
||||
|
||||
def add_category(self):
|
||||
self.save_category()
|
||||
cat_name = unicode(self.input_box.text()).strip()
|
||||
if cat_name == '':
|
||||
return False
|
||||
if cat_name not in self.categories:
|
||||
self.category_box.clear()
|
||||
self.current_cat_name = cat_name
|
||||
self.categories[cat_name] = []
|
||||
self.applied_items = []
|
||||
self.populate_category_list()
|
||||
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
|
||||
else:
|
||||
self.select_category(self.category_box.findText(cat_name))
|
||||
return True
|
||||
|
||||
def del_category(self):
|
||||
if self.current_cat_name is not None:
|
||||
if not confirm('<p>'+_('The current tag category will be '
|
||||
'<b>permanently deleted</b>. Are you sure?')
|
||||
+'</p>', 'tag_category_delete', self):
|
||||
return
|
||||
del self.categories[self.current_cat_name]
|
||||
self.current_cat_name = None
|
||||
self.category_box.removeItem(self.category_box.currentIndex())
|
||||
|
||||
def select_category(self, idx):
|
||||
self.save_category()
|
||||
s = self.category_box.itemText(idx)
|
||||
if s:
|
||||
self.current_cat_name = unicode(s)
|
||||
else:
|
||||
self.current_cat_name = None
|
||||
if self.current_cat_name:
|
||||
self.applied_items = [cat[2] for cat in self.categories.get(self.current_cat_name, [])]
|
||||
else:
|
||||
self.applied_items = []
|
||||
self.display_filtered_categories(None)
|
||||
|
||||
def accept(self):
|
||||
self.save_category()
|
||||
prefs['user_categories'] = self.categories
|
||||
QDialog.accept(self)
|
||||
|
||||
def save_category(self):
|
||||
if self.current_cat_name is not None:
|
||||
l = []
|
||||
for index in self.applied_items:
|
||||
item = self.all_items[index]
|
||||
l.append([item.name, item.label, item.index])
|
||||
self.categories[self.current_cat_name] = l
|
||||
|
||||
def populate_category_list(self):
|
||||
for n in sorted(self.categories.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower())):
|
||||
self.category_box.addItem(n)
|
385
src/calibre/gui2/dialogs/tag_categories.ui
Normal file
@ -0,0 +1,385 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TagCategories</class>
|
||||
<widget class="QDialog" name="TagCategories">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>588</width>
|
||||
<height>482</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>User Categories Editor</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="1" column="0">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>A&vailable items</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>available_items_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="available_items_box">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::MultiSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="apply_button">
|
||||
<property name="toolTip">
|
||||
<string>Apply tags to current tag category</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/forward.svg</normaloff>:/images/forward.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>A&pplied items</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>applied_items_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="applied_items_box">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::MultiSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="unapply_button">
|
||||
<property name="toolTip">
|
||||
<string>Unapply (remove) tag from current tag category</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/list_remove.svg</normaloff>:/images/list_remove.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="4">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="4">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Category name: </string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>category_box</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="category_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>160</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>145</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Select a category to edit</string>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QToolButton" name="delete_category_button">
|
||||
<property name="toolTip">
|
||||
<string>Delete this selected tag category</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QLineEdit" name="input_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>60</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Enter a new category name. Select the kind before adding it.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="5">
|
||||
<widget class="QToolButton" name="add_category_button">
|
||||
<property name="toolTip">
|
||||
<string>Add the new category</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="5">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Category filter: </string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="category_filter_box">
|
||||
<property name="toolTip">
|
||||
<string>Select the content kind of the new category</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../../calibre_datesearch/resources/images"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>TagCategories</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>TagCategories</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -4,12 +4,14 @@ from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog
|
||||
|
||||
from calibre.gui2.dialogs.tag_editor_ui import Ui_TagEditor
|
||||
from calibre.gui2 import qstring_to_unicode
|
||||
from calibre.gui2 import question_dialog, error_dialog
|
||||
from calibre.constants import islinux
|
||||
|
||||
class TagEditor(QDialog, Ui_TagEditor):
|
||||
|
||||
def tag_cmp(self, x, y):
|
||||
return cmp(x.lower(), y.lower())
|
||||
|
||||
def __init__(self, window, db, index=None):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagEditor.__init__(self)
|
||||
@ -23,7 +25,7 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
tags = []
|
||||
if tags:
|
||||
tags = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
||||
tags.sort()
|
||||
tags.sort(cmp=self.tag_cmp)
|
||||
for tag in tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
else:
|
||||
@ -33,7 +35,7 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
|
||||
all_tags = [tag for tag in self.db.all_tags()]
|
||||
all_tags = list(set(all_tags))
|
||||
all_tags.sort()
|
||||
all_tags.sort(cmp=self.tag_cmp)
|
||||
for tag in all_tags:
|
||||
if tag not in tags:
|
||||
self.available_tags.addItem(tag)
|
||||
@ -57,30 +59,30 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
|
||||
return
|
||||
for item in items:
|
||||
if self.db.is_tag_used(qstring_to_unicode(item.text())):
|
||||
if self.db.is_tag_used(unicode(item.text())):
|
||||
confirms.append(item)
|
||||
else:
|
||||
deletes.append(item)
|
||||
if confirms:
|
||||
ct = ', '.join([qstring_to_unicode(item.text()) for item in confirms])
|
||||
ct = ', '.join([unicode(item.text()) for item in confirms])
|
||||
if question_dialog(self, _('Are your sure?'),
|
||||
'<p>'+_('The following tags are used by one or more books. '
|
||||
'Are you certain you want to delete them?')+'<br>'+ct):
|
||||
deletes += confirms
|
||||
|
||||
for item in deletes:
|
||||
self.db.delete_tag(qstring_to_unicode(item.text()))
|
||||
self.db.delete_tag(unicode(item.text()))
|
||||
self.available_tags.takeItem(self.available_tags.row(item))
|
||||
|
||||
|
||||
def apply_tags(self, item=None):
|
||||
items = self.available_tags.selectedItems() if item is None else [item]
|
||||
for item in items:
|
||||
tag = qstring_to_unicode(item.text())
|
||||
tag = unicode(item.text())
|
||||
self.tags.append(tag)
|
||||
self.available_tags.takeItem(self.available_tags.row(item))
|
||||
|
||||
self.tags.sort()
|
||||
self.tags.sort(cmp=self.tag_cmp)
|
||||
self.applied_tags.clear()
|
||||
for tag in self.tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
@ -90,19 +92,24 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
def unapply_tags(self, item=None):
|
||||
items = self.applied_tags.selectedItems() if item is None else [item]
|
||||
for item in items:
|
||||
tag = qstring_to_unicode(item.text())
|
||||
tag = unicode(item.text())
|
||||
self.tags.remove(tag)
|
||||
self.available_tags.addItem(tag)
|
||||
|
||||
self.tags.sort()
|
||||
self.tags.sort(cmp=self.tag_cmp)
|
||||
self.applied_tags.clear()
|
||||
for tag in self.tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
|
||||
self.available_tags.sortItems()
|
||||
items = [unicode(self.available_tags.item(x).text()) for x in
|
||||
range(self.available_tags.count())]
|
||||
items.sort(cmp=self.tag_cmp)
|
||||
self.available_tags.clear()
|
||||
for item in items:
|
||||
self.available_tags.addItem(item)
|
||||
|
||||
def add_tag(self):
|
||||
tags = qstring_to_unicode(self.add_tag_input.text()).split(',')
|
||||
tags = unicode(self.add_tag_input.text()).split(',')
|
||||
for tag in tags:
|
||||
tag = tag.strip()
|
||||
for item in self.available_tags.findItems(tag, Qt.MatchFixedString):
|
||||
@ -110,7 +117,7 @@ class TagEditor(QDialog, Ui_TagEditor):
|
||||
if tag not in self.tags:
|
||||
self.tags.append(tag)
|
||||
|
||||
self.tags.sort()
|
||||
self.tags.sort(cmp=self.tag_cmp)
|
||||
self.applied_tags.clear()
|
||||
for tag in self.tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
|
128
src/calibre/gui2/dialogs/tag_list_editor.py
Normal file
@ -0,0 +1,128 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from functools import partial
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QListWidgetItem
|
||||
|
||||
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
||||
from calibre.gui2 import question_dialog, error_dialog
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
|
||||
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
|
||||
def __init__(self, window, db, tag_to_match, category):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_TagListEditor.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.to_rename = {}
|
||||
self.to_delete = []
|
||||
self.db = db
|
||||
self.all_tags = {}
|
||||
self.category = category
|
||||
if category == 'tags':
|
||||
result = db.get_tags_with_ids()
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
elif category == 'series':
|
||||
result = db.get_series_with_ids()
|
||||
compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
|
||||
elif category == 'publisher':
|
||||
result = db.get_publishers_with_ids()
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
else: # should be a custom field
|
||||
self.cc_label = None
|
||||
if category in db.field_metadata:
|
||||
self.cc_label = db.field_metadata[category]['label']
|
||||
result = self.db.get_custom_items_with_ids(label=self.cc_label)
|
||||
else:
|
||||
result = []
|
||||
compare = (lambda x,y:cmp(x.lower(), y.lower()))
|
||||
|
||||
for k,v in result:
|
||||
self.all_tags[v] = k
|
||||
for tag in sorted(self.all_tags.keys(), cmp=compare):
|
||||
item = QListWidgetItem(tag)
|
||||
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||
self.available_tags.addItem(item)
|
||||
|
||||
items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
|
||||
if len(items) == 1:
|
||||
self.available_tags.setCurrentItem(items[0])
|
||||
|
||||
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
|
||||
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
|
||||
self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
|
||||
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
||||
|
||||
def finish_editing(self, item):
|
||||
if not item.text():
|
||||
error_dialog(self, _('Item is blank'),
|
||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||
item.setText(self.item_before_editing.text())
|
||||
return
|
||||
if item.text() != self.item_before_editing.text():
|
||||
if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
|
||||
error_dialog(self, _('Item already used'),
|
||||
_('The item %s is already used.')%(item.text())).exec_()
|
||||
item.setText(self.item_before_editing.text())
|
||||
return
|
||||
(id,ign) = self.item_before_editing.data(Qt.UserRole).toInt()
|
||||
self.to_rename[item.text()] = id
|
||||
|
||||
def rename_tag(self):
|
||||
item = self.available_tags.currentItem()
|
||||
self._rename_tag(item)
|
||||
|
||||
def _rename_tag(self, item):
|
||||
if item is None:
|
||||
error_dialog(self, _('No item selected'),
|
||||
_('You must select one item from the list of Available items.')).exec_()
|
||||
return
|
||||
self.item_before_editing = item.clone()
|
||||
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
||||
self.available_tags.editItem(item)
|
||||
|
||||
def delete_tags(self, item=None):
|
||||
deletes = self.available_tags.selectedItems() if item is None else [item]
|
||||
if not deletes:
|
||||
error_dialog(self, _('No items selected'),
|
||||
_('You must select at least one items from the list.')).exec_()
|
||||
return
|
||||
ct = ', '.join([unicode(item.text()) for item in deletes])
|
||||
if not question_dialog(self, _('Are your sure?'),
|
||||
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
|
||||
return
|
||||
|
||||
for item in deletes:
|
||||
(id,ign) = item.data(Qt.UserRole).toInt()
|
||||
self.to_delete.append(id)
|
||||
self.available_tags.takeItem(self.available_tags.row(item))
|
||||
|
||||
def accept(self):
|
||||
rename_func = None
|
||||
if self.category == 'tags':
|
||||
rename_func = self.db.rename_tag
|
||||
delete_func = self.db.delete_tag_using_id
|
||||
elif self.category == 'series':
|
||||
rename_func = self.db.rename_series
|
||||
delete_func = self.db.delete_series_using_id
|
||||
elif self.category == 'publisher':
|
||||
rename_func = self.db.rename_publisher
|
||||
delete_func = self.db.delete_publisher_using_id
|
||||
else:
|
||||
rename_func = partial(self.db.rename_custom_item, label=self.cc_label)
|
||||
delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label)
|
||||
|
||||
work_done = False
|
||||
if rename_func:
|
||||
for text in self.to_rename:
|
||||
work_done = True
|
||||
rename_func(id=self.to_rename[text], new_name=unicode(text))
|
||||
for item in self.to_delete:
|
||||
work_done = True
|
||||
delete_func(item)
|
||||
if not work_done:
|
||||
QDialog.reject(self)
|
||||
else:
|
||||
QDialog.accept(self)
|
163
src/calibre/gui2/dialogs/tag_list_editor.ui
Normal file
@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TagListEditor</class>
|
||||
<widget class="QDialog" name="TagListEditor">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>397</width>
|
||||
<height>335</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Category Editor</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Items in use</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>available_tags</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QToolButton" name="delete_button">
|
||||
<property name="toolTip">
|
||||
<string>Delete item from database. This will unapply the item from all books and then remove it from the database.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="rename_button">
|
||||
<property name="toolTip">
|
||||
<string>Rename the item in every book where it is used.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="available_tags">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>TagListEditor</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>TagListEditor</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -9,7 +9,7 @@ from PyQt4.Qt import SIGNAL, QUrl, QDesktopServices, QAbstractListModel, Qt, \
|
||||
from calibre.web.feeds.recipes import compile_recipe
|
||||
from calibre.web.feeds.news import AutomaticNewsRecipe
|
||||
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
||||
from calibre.gui2 import qstring_to_unicode, error_dialog, question_dialog, \
|
||||
from calibre.gui2 import error_dialog, question_dialog, \
|
||||
choose_files, ResizableDialog, NONE
|
||||
from calibre.gui2.widgets import PythonHighlighter
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
@ -162,19 +162,19 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
||||
else:
|
||||
self.stacks.setCurrentIndex(1)
|
||||
self.toggle_mode_button.setText(_('Switch to Basic mode'))
|
||||
if not qstring_to_unicode(self.source_code.toPlainText()).strip():
|
||||
if not unicode(self.source_code.toPlainText()).strip():
|
||||
src = self.options_to_profile()[0].replace('AutomaticNewsRecipe', 'BasicNewsRecipe')
|
||||
self.source_code.setPlainText(src.replace('BasicUserRecipe', 'AdvancedUserRecipe'))
|
||||
self.highlighter = PythonHighlighter(self.source_code.document())
|
||||
|
||||
|
||||
def add_feed(self, *args):
|
||||
title = qstring_to_unicode(self.feed_title.text()).strip()
|
||||
title = unicode(self.feed_title.text()).strip()
|
||||
if not title:
|
||||
error_dialog(self, _('Feed must have a title'),
|
||||
_('The feed must have a title')).exec_()
|
||||
return
|
||||
url = qstring_to_unicode(self.feed_url.text()).strip()
|
||||
url = unicode(self.feed_url.text()).strip()
|
||||
if not url:
|
||||
error_dialog(self, _('Feed must have a URL'),
|
||||
_('The feed %s must have a URL')%title).exec_()
|
||||
@ -190,7 +190,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
||||
|
||||
def options_to_profile(self):
|
||||
classname = 'BasicUserRecipe'+str(int(time.time()))
|
||||
title = qstring_to_unicode(self.profile_title.text()).strip()
|
||||
title = unicode(self.profile_title.text()).strip()
|
||||
if not title:
|
||||
title = classname
|
||||
self.profile_title.setText(title)
|
||||
@ -229,7 +229,7 @@ class %(classname)s(%(base_class)s):
|
||||
return
|
||||
profile = src
|
||||
else:
|
||||
src = qstring_to_unicode(self.source_code.toPlainText())
|
||||
src = unicode(self.source_code.toPlainText())
|
||||
try:
|
||||
title = compile_recipe(src).title
|
||||
except Exception, err:
|
||||
|
@ -15,10 +15,11 @@ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
||||
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre.gui2 import Dispatcher, error_dialog, NONE, config
|
||||
from calibre.gui2 import Dispatcher, error_dialog, NONE, config, gprefs
|
||||
from calibre.gui2.device import DeviceJob
|
||||
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
||||
from calibre import __appname__
|
||||
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||
|
||||
class JobManager(QAbstractTableModel):
|
||||
|
||||
@ -243,7 +244,32 @@ class ProgressBarDelegate(QAbstractItemDelegate):
|
||||
opts.text = QString(_('Unavailable') if percent == 0 else '%d%%'%percent)
|
||||
QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)
|
||||
|
||||
class DetailView(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, parent, job):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(job.description)
|
||||
self.job = job
|
||||
self.next_pos = 0
|
||||
self.update()
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update)
|
||||
self.timer.start(1000)
|
||||
|
||||
|
||||
def update(self):
|
||||
f = self.job.log_file
|
||||
f.seek(self.next_pos)
|
||||
more = f.read()
|
||||
self.next_pos = f.tell()
|
||||
if more:
|
||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
||||
|
||||
|
||||
|
||||
class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
|
||||
def __init__(self, window, model):
|
||||
QDialog.__init__(self, window)
|
||||
Ui_JobsDialog.__init__(self)
|
||||
@ -252,8 +278,6 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.model = model
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setWindowTitle(__appname__ + _(' - Jobs'))
|
||||
self.connect(self.jobs_view.model(), SIGNAL('modelReset()'),
|
||||
self.jobs_view.resizeColumnsToContents)
|
||||
self.connect(self.kill_button, SIGNAL('clicked()'),
|
||||
self.kill_job)
|
||||
self.connect(self.details_button, SIGNAL('clicked()'),
|
||||
@ -264,7 +288,21 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.jobs_view.model().kill_job)
|
||||
self.pb_delegate = ProgressBarDelegate(self)
|
||||
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
|
||||
self.jobs_view.doubleClicked.connect(self.show_job_details)
|
||||
self.jobs_view.horizontalHeader().setMovable(True)
|
||||
state = gprefs.get('jobs view column layout', None)
|
||||
if state is not None:
|
||||
try:
|
||||
self.jobs_view.horizontalHeader().restoreState(bytes(state))
|
||||
except:
|
||||
pass
|
||||
|
||||
def show_job_details(self, index):
|
||||
row = index.row()
|
||||
job = self.jobs_view.model().row_to_job(row)
|
||||
d = DetailView(self, job)
|
||||
d.exec_()
|
||||
d.timer.stop()
|
||||
|
||||
def kill_job(self):
|
||||
for index in self.jobs_view.selectedIndexes():
|
||||
@ -274,12 +312,16 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
|
||||
def show_details(self):
|
||||
for index in self.jobs_view.selectedIndexes():
|
||||
self.jobs_view.show_details(index)
|
||||
self.show_job_details(index)
|
||||
return
|
||||
|
||||
def kill_all_jobs(self):
|
||||
self.model.kill_all_jobs()
|
||||
|
||||
def closeEvent(self, e):
|
||||
self.jobs_view.write_settings()
|
||||
try:
|
||||
state = bytearray(self.jobs_view.horizontalHeader().saveState())
|
||||
gprefs['jobs view column layout'] = state
|
||||
except:
|
||||
pass
|
||||
e.accept()
|
||||
|
10
src/calibre/gui2/library/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/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 PyQt4.Qt import Qt
|
||||
|
||||
DEFAULT_SORT = ('timestamp', Qt.DescendingOrder)
|