From a5c35c3b930cc5de02f8145b6dc51217c1dcbe58 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 15:48:58 -0700
Subject: [PATCH 01/61] SONY driver: Don't abort when failing to upload a cover
---
src/calibre/devices/prs505/driver.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 874fbe4b10..e9329c39c6 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -229,7 +229,11 @@ def upload_cover(self, path, filename, metadata, filepath):
debug_print('PRS505: not uploading cover')
return
debug_print('PRS505: uploading cover')
- self._upload_cover(path, filename, metadata, filepath)
+ try:
+ self._upload_cover(path, filename, metadata, filepath)
+ except:
+ import traceback
+ traceback.print_exc()
def _upload_cover(self, path, filename, metadata, filepath):
if metadata.thumbnail and metadata.thumbnail[-1]:
From 4b25d9ef0d47ae7efcd839f072f1f16dd87805fc Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 16:02:42 -0700
Subject: [PATCH 02/61] Fix Heuristics preference widget
---
resources/images/heuristics.png | Bin 0 -> 9561 bytes
src/calibre/gui2/convert/heuristics.py | 22 +--
src/calibre/gui2/convert/heuristics.ui | 150 ++++++++++++------
.../gui2/convert/structure_detection.py | 2 +-
4 files changed, 103 insertions(+), 71 deletions(-)
create mode 100644 resources/images/heuristics.png
diff --git a/resources/images/heuristics.png b/resources/images/heuristics.png
new file mode 100644
index 0000000000000000000000000000000000000000..92c53ae8ff9c70a394afffd4a644bd34e77c70ea
GIT binary patch
literal 9561
zcmb7KRZtvEklsZWcL=VFlVFPlT_m`>OOW6Y+*u?9NN^I|gKG%x76|U{5FqH{ve@xI
z-P1i>P0e)G)YQy3)zjTm-CvZtsvHg`1ttIhz)_Hw(R`7x|2jJ8W$YEV7=96;_eydy
zfam{OZhLVO06+&&kdf5($vh79%_N?!f4EPxo8ILR(MOAvFy{O6@Gzf!ev0g8-3^J}2+g-ec?ZRpfm*?A8cfSh#NcsD
z&N_NG9yi!ume$9Mi_6QMSB*T!n%CXO+8ga*4AHFlk^hfj>kDA$CcIE7MOUfxF+43U
zQFD$L0)V1pIAqWl){lN9`o{f%l6h4m#)yYk=i!hTS;cDvFPX~HbDoF
z?lvJZfM~#dJJ6hj&XM{%a4#qLX*a;SK?0lz#~4BA0CN7ZJ`DpH$^fo)Mepu;FXAHq
zVYy!z&c6%Yu=IQ46&EDqby;ic4-6y_i$cPN4gEK#$GSdGI+65e85Kpt$Y@nxM`uxO
zns4+BQzq8LKsDW9b?+E&DW`~jV;)1&44zqSM=>a}cPjJFK^~>0DlAQU
z91Rv;Y9aUeH#@&|_z)omn{|q!UZL8o*>eP|S=U$tJ?QXQ+}w
z0Df(z1;@lu!J)7pYV&jF3Fi{_3k3$OcOM%E?HS5W^T{v}ibG80RGZNSo8DlYWOG*yixUQksq=-5)l-4&qx9Q2=wQnVBcP-Q5s0k
zvaT`$z{ZGcJleYs)R-b@b8lFsPvZWag~Vq6yOK%Xz3B~w42BB|Adox5?6+BCkg&^_
zjgtNVlGg#bM6T-XU48UfwiU|Fz^NrBN4d0%GUS9VpjZieYn2Fy;-9Fs34Ikl5+J>SQnhX
z42pN*f=82w^5w8e1iORj?P`{d4u^0q%L&bWfK;U}`Ap&0
zu|*NY`WX11P*4~&w94B)>J|Ll3SsKs|D^PW`j?qvtod$xw%^~?4TtJC{*#7fOx+`V
z@B3;npyRPuBKMG(2`3j8(Yq=)8XyavowQT4pWKjZ&94l3F>JVAZbkcQHe=f_{^&|4
zf?zSu+nkDvnc+AtqZ}_wU-PkPV*)Or^b(zGsmT
zGd=9nA-TUwn;n{2P?hHC?RLbBwL@kIBCDXI_S&XebEAnKq)L_!{X|V$a;#Y7BA-{8
z-pGYuZsRwupDg11G(W1o3zukPKQqYCThGJ6pyC%QT>&meU;Wq~zSptl5HsdyReL-o
z4{NPNUn`wA>v!1uC18GrKsOZD4sxR>zg3b^dfZAW)(QO1Tmh)cSoQ+QF?b(@f1$e8
zFaW|}{l%*Rx_1{Qvm_BT{4iry2{UQ^4h$cgPagYqGAXHiWh8|JjhzU#Md8Wyf^QPt
zYlq*}Q4h;|%im@K+h{o_
z>qP4NWFh`y;o}Tau}vkedK
zH_rIQGC>vuVvF}Z%u%Ur!kMDotzur&D&?^rV0UAUY2a6QToRe!M2oDTdL3_sVYet)
zGtKzpFeJ*wgK2x2Sx2=0wHk5ad(~)LKXU1uAF}Q(WngT7Myx;l?T7a3`SnmEsg^L#
zdTZu%3T45>n|JVTAT1PQyIqAv$7)B8j{I^~Wv!~tUw0hWjct)!x)hfRqP^7vW{Q|u
z9=a)xRbk%Vw|iE8-E>DgDBWvyv~hjGzRqWm&3vTX&5r^^4}nw#7;j^RniRiXW9
z{QQBVvpwtd;B(KsLoE3M#;6VjThb$T^>$Lqd4dsCL)3pUbID()AU}7@K%*vjKW(`x
zFkcEu@m=^5=EhOAVx~V?K|^MsT|t&`c2A#+phQ+Gza?*Sxv
zS^z2g#vKvD#RAS;^)ft>>k&+%h$$`5JR?u8EIXoM;LcdZS1eIMr=`;L6GDs$EwwKS
zBU$6!xdSnv_r}AgE3)lSYhl3oGXT5F@?pbvBbnL3V^T1Zk-)m2B+AErHCWFp+~cbRDHgVH%>jGEzcI?E~wH|SF*rBt
z0zfmgZeIx@nDy3Z>-X$F#OD(zxH^EfhG`cysE{D)Co*C7k$j%xK$eK2W=jpxpwZ_C
z)n&DdirawmX0n?AmfD*#*X5fB@w?bN(a(f*Es|yLVKF*gsM|1z9MGCz_6;{9MB$W4
z0erUd_w7>@-85e?3*(kOmARrVZ{gk%2$L69>6DPjkxj{Kb2c#h2-w*DP^tYU>3tTw
z3Of%x_+rPdDLx7qR3bG*Ou}v1+h4Y*B~GqXtBactuH^nX7JqaO*lof(C$4S^u$D~l
z2A+uXcbI>Bz43TO8TTR8^wL%l17ztDY(9jtk(xKBVV3(xsN)27tF42F_grG%%I0f9
zhYPbf&xXK$Q~J-EAjZ4ps+M)OWXs?#)}oJ`AG95-zMhU?hFmkYVsZJUU{OsC)S&K7
zRuZWjwEV#zlqT>49q2(owI2K{XXB2Jp~Z=($8BkwXz-+rMe(l4!xKL
z>ETwt2WDvf`xnZOT=!4}H)D1kFF;p9uF!##%9k8^m%P9)$aNsn(mOG|`@)Sa=Sj=r?F+V4XoNgqq=#q|ZS
z-;zVu%>oP;56>6OJh#Yy5ZGCnR18%dHb7{MnB6t~TxkKK4I{8p^|kKyBr7J|m<
za?zZV;-K+;cI;gCk?C}$i#f@-L@`tp+51qj^v7s-l79j}nh56UDjkwErD`v&v3@*!
zoK{5=I5~Y)xTG{8fa*DYX}2$|l`n>m1}$PO=0`T6fIfbE1O95Ym_ZQJNjX`uqB9Th
zG@1v*z{tnRBPnB*-5@9cpL@%Y8v%cZbN=_rqTDB#D=NK
z2Bl{xq$jT5{9BJgzRctesQ^vv4VmoOM!ufqr>Xj&-qEr;HRf|mlAs0mj^_=LODUpT
zys(hN`&!VV&3<`E$_g;^*uCODTqbaH)n6D#nZxrYZ>D&Y=vEXsL9M{pX%TN&I^{p8
z(SkqMM}92J|CIpIyA}7
zLpR0Nc*D617N|dr7&zJ-U*USyL7vWHH!0x~`?;zz{H-o6Ces&-6_-|V=Wiz*NIDPw
zqA!2vIxhfE*?D^LUSxv*q&DrRc?b>FW>v2=8dP;Mkw|o9bAEiw~RXG~u
zhJ#ZSqL{TZgt_P#%|qS%YGywGq^s8gi5<37OnUpcC304Ad}Pli2$7eUg)FPRPZxMO
zY+PRlLcc|61Zq|hCO_>e&1*{rrHQvo@m9-1TQwsMEhS-UhcR*)U6_>{f_0@0;
zFdNr=u{-lnNSx>47x%GBT4P4*J^Br6&$j7QT2CQFk$j7P$AG5(fI1BGieauhLI0Kw^>qe!_r#p|0V?PYk;Of0UsdbiXRct9j
zI93Y^M_Z);AH>Gx7EBqdZv$_J`6)zhQH})TdBWpQ2J(H@_p1`ccK`f_FI%Uc-nx{R1*8am!WGk})%$>XawE`qmoBZCX{8s7Ccq`RKAUZRKZ0AinUR_&^-qq=p
z;_qQcm_lV1+I_We0fL;KawwqhCo-i|Y?W}ufUWWQrgHZw_-bj%em;ucq1e}t)a-(c
zf3uBj43RYO4z}RcR`f1a_YtBr@S-J}?~9roCF5P2mIJ?)w$WsLLVS}bTW7)l?i{~R
z(|cn$gYxs~qJjd&9Fx;~>Fg*o$s`L6U?}IgT#z1t^{ddQB_g$Iv$3T=1M(ZteZhp>
zX+j)?^F=p9zi}1AjZ9b}Gr*jZ43>;d&xGA8GsK;sTokWN&aDZ;mhQr54#&sWlv=7T
zN%3MFuL)JSSp`hRf>SM2*lRPK^zF!vJTWkO`7F@wo^9m6pSm^VKI3Z9t%RCr21l;P
zWqf7%uC&fA1R3?aw@eW7L!1o6rmy*V$|A@A+cP5tp!YW@?*xK!9Uj7Is^nww-%MeU
zUY}H)box16k+jHONiNYYn#pP72}b+66rO5)8PA_gdoOyP{`?7B4(AA}QaUn`QE|a|
zBuPwffr&9q%>BulwTRxFjQIsQ-|>18#x$QrDpCYKei6)$V_9*W%yDmLZrc`!${rn-
z-((K=MC-%M4|4H;!jye|f)j?-Kld$Tl$!465dNi;)>Z?!sQ!spIZDbcc>4JW5I)nm
zYXxvw*S=_sj9?Kfobod%Z~z-xZA9D666vXy`(D=jvx*})9+TGHJ)kZ@Qf^g=-2wz%N+(!{J8^;uMp9o6ksrL-s`fx|LG7>gA`
zyD^(aLo`7tNlx*X&yj$0Szb8R^vck)b5#c=Kvo4##Taf-8VNvwfq9)4gs=$!K35Sf
zF@UjQU3HeXL!bj8K>HKO)hk!G$mTSI^t+_#p|rvKeDMEx|vNMm|}&L)x=d9;|I!#LA*zr6mN}3R#az)^TrOI%FS*R3=&Z<
zIs7j78bf^N1HNt3S$yXW=-{8g6LKd~8A(wMdsQrv5jctzoN;ho|1RVp65jTe~C2q7A(3B(g
zd8a6#zK{W<>jqT$rQb-|bsbAVXt;%rt(RA^1RnlN&OY-
zNw9d-m;RJoZ!R1>RyGT}@Q;X^>rm7(_D=PK@xM2Z?b%aH!z-$=z))v)S5yDcvxSVQ
zc{cfiet_u_?^f{~R=Cg+sJGhDk?-=S?D(Gbx5A99PVY#|r9>WKh%u9#V
z+KnWP!a1ygq8-NXy^XW{yCNE*qbC+SV!ZV_g*Wzo`U1uVe<&A=wSuXS1#Q)`b;Cfvl=Y&YM=SLD5JYk#Y%MG!h)-A&ye$rR(ds
zD_&DogChK{rYFi*FTplqzwIAVJRz5uNWHzYfPr(GPP?IekhaN#T^TsId$Fr)1@^eo
zwK6p|Ge4)+n2h%qQDts31wWoxxdSZY{d;=%(Jr-_4k+!@9&AO6Y)O$s?yr@HbT^l#PkX*ZJUM47@*!D|-n%cTsM$aax0J&G@SJj0#Fj@Hc*1C{4#ATUoqc~?p
z=ia<#ER)Q9YWryoK7wYIactDq-lOu*wdaj;uEAr6o9|}77IbJH
zr-+x)Voa$hIFIF20;}raO7+!QYvgFvvMu@u&$M3~B9!gW$RgBf>aSMM3w&EZT#gz;
zzJ~tSkWBu3qHh{jq^$948N?sHlV05)sE;3lu8MiIBf22ST4E3BA=f`@VMzL`
z2710`%)TF_OaHbKQkaZE#gb=MoZqTaa2}?v-X|Cz3}pd50=CRppbJjPh3#_BTuNHD
zYJSg%Kxlnb5?(ZE<~fPt<60Gdry1*u!&0mSQ)&gCmiGOEr_Twad{9BUO>g960t@cn
z|2*toYwhL(|DBMZpnO6gatrjBde~^j6pM-`yr|;vHq#ppE3A>?RV9R*>Wj-0?G+V!8#&(C!~?d$BSg
zrz!Y!S9V<@pIb$1nC*BtS^`%Ef6@a0xNth_Uz%Q~sc;@_OwYstZ0xkiR-nE5@^nx1
zywuy>&VOU$?Pc%d8%E813B_G?I?y{nkNJz0`{c2EdLWCiVdjA#*JWVuvf~xKgOHo#80d81i+>=+PXj15mXCiyyu!3m^5a#tY+xK@^+{sG*KtorhcH;sI&VI_v$kb2~6&L`YmJs32TY@(bx3zB4g_{CYoaHhmfTT
zaHI)+^>Z`{a5RY`PDc+tgK$ZDJm_+>OL!U
z%>fGHt|AKb`zVQY#ZmabhHF*k)XMa9Sw=tw@7%B7P0zaf6QBmhBL&D5EUouu11s~OZobVH9F^^9TeznACWCpy%meluyx2l51_t7P4+Mhs+gbd#;25KKP4IQSHL
zEgQcrib-SAD}O6)8k>!wfy9NiT@M0i=+krA{;c;-b+%Ys8#w?FbmtR50K-@t$-Qqk
zVBtB5y?9Z+=-z}4=i=(C45WNJK-mn|1iDc3U|;eX+l;R3N%Tc?oG}iQqgfFRHD72I
z`Cm93edYE?qiO_9HECq?Fi0zBntioBCKP+)z;}>Sp&+}dIl-utLL7A*@s-K0;!5@GOYaB1hE9UV{
zTT4rO0!SR%%pPEc#m30s+!``gervRAm`YK6A|8j~jYLM(mJdc#GUlJ`lDq<@kIOz9
ztbCp}y0DHnURtb4`q*EgO4ieg%FuFJuxwBBM!Ww7G9>`OC$an%zvx;R_H+Vr11h2pY6D0}w{H9s5hSFLyKx24
z=MF(Kx!e?qj$a6rTQi2n^*p1ODu1JB78PftnN=>q$w5w?Tt5yH4RI*^cC>ki5;8k`
z-|2IfejHxIbe^4
z5W(i)m9E&LBp~kpoBT4C>%ZrQMP1|M)E(vH5#=l?3znq8ASOG}9#ZoAcRmQ$6
z4KD#mAI#9E)0o4-h>mrH=n65PT3WeFco0(
zitZQ1>XP>^4kRr=0|;gTzusfMMDMi6AOY~Ld3j%-h)BRQE8$o7_rDIGvKw=+>90$G
zfVtk2U)gHeNE18QnHaL#JmFSGbwBhj`b0?~%Dc92e{-U3&I1CT%|4?GZL4qeL-?>O
zj@hBu%PDNo@Nu6G{#K$d^XOcbOi$}`_x?G$HM!RW*G<5ZreOMc2ze5tD=vfCazA4Z)j`ZvL~;amdsWsWw_v8e
z>YeQ_4&{9d86xdyq5#;(cLAyt7OR%=EDpu2pzyZkWTgTA}*#b
zE8qa@KmF`e@b1xemW&
zBAYDvtSNykbaf)McKhe>A9i|UBGx^{0
zjk_Dv-Hv^%hlZ5zd29l|I2-0UJ@B@aoL+9DV63@T_odAo=E4jDc7y`tl5$a+kjg>|+OGmr0l
zSi3ho32RxJl>!P%HeTbEN?9UfmNB=jW(+dBpX}%;`ad~GJ*UBud$MvC7W7;%Tq%IU
MJ5`w~Df7_(0eS^uMgRZ+
literal 0
HcmV?d00001
diff --git a/src/calibre/gui2/convert/heuristics.py b/src/calibre/gui2/convert/heuristics.py
index 0655d7400f..e788888257 100644
--- a/src/calibre/gui2/convert/heuristics.py
+++ b/src/calibre/gui2/convert/heuristics.py
@@ -11,9 +11,10 @@
class HeuristicsWidget(Widget, Ui_Form):
- TITLE = _('Heuristic Processing')
+ TITLE = _('Heuristic\nProcessing')
HELP = _('Modify the document text and structure using common patterns.')
COMMIT_NAME = 'heuristics'
+ ICON = I('heuristics.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
@@ -46,23 +47,8 @@ def set_value_handler(self, g, val):
return True
def enable_heuristics(self, state):
- if state == Qt.Checked:
- state = True
- else:
- state = False
- self.opt_markup_chapter_headings.setEnabled(state)
- self.opt_italicize_common_cases.setEnabled(state)
- self.opt_fix_indents.setEnabled(state)
- self.opt_delete_blank_paragraphs.setEnabled(state)
- self.opt_format_scene_breaks.setEnabled(state)
- self.opt_dehyphenate.setEnabled(state)
- self.opt_renumber_headings.setEnabled(state)
-
- self.opt_unwrap_lines.setEnabled(state)
- if state and self.opt_unwrap_lines.checkState() == Qt.Checked:
- self.opt_html_unwrap_factor.setEnabled(True)
- else:
- self.opt_html_unwrap_factor.setEnabled(False)
+ state = state == Qt.Checked
+ self.heuristic_options.setEnabled(state)
def enable_unwrap(self, state):
if state == Qt.Checked:
diff --git a/src/calibre/gui2/convert/heuristics.ui b/src/calibre/gui2/convert/heuristics.ui
index 8048bef204..4358512996 100644
--- a/src/calibre/gui2/convert/heuristics.ui
+++ b/src/calibre/gui2/convert/heuristics.ui
@@ -6,7 +6,7 @@
0
0
- 938
+ 724
470
@@ -15,114 +15,160 @@
-
-
+
- &Preprocess input file to possibly improve structure detection
+ <b>Heuristic processing</b> means that calibre will scan your book for common patterns and fix them. As the name implies, this involves guesswork, which means that it could end up worsening the result of a conversion, if calibre guesses wrong. Therefore, it is disabled by default. Often, if a conversion does not turn out as you expect, turning on heuristics can improve matters.
+
+
+ true
-
-
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 15
+
+
+
+
+ -
+
+
+ Enable &heuristic processing
+
+
+
+ -
+
Heuristic Processing
-
-
-
+
+
-
Unwrap lines
- -
-
-
- Line &un-wrap factor during preprocess:
-
-
- opt_html_unwrap_factor
-
-
+
-
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Line &un-wrap factor :
+
+
+ opt_html_unwrap_factor
+
+
+
+ -
+
+
+
+
+
+ 1.000000000000000
+
+
+ 0.050000000000000
+
+
+ 0.400000000000000
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
- -
-
-
-
-
-
- 1.000000000000000
-
-
- 0.050000000000000
-
-
- 0.400000000000000
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
+
-
Detect and markup unformatted chapter headings and sub headings
- -
+
-
Renumber sequences of <h1> or <h2> tags to prevent splitting
- -
+
-
Delete blank lines between paragraphs
- -
+
-
Ensure scene breaks are consistently formatted
- -
+
-
Remove unnecessary hyphens
- -
+
-
Italicize common words and patterns
- -
+
-
Replace entity indents with CSS indents
- -
+
-
Qt::Vertical
diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py
index 2c64303ee7..d8e2f4f122 100644
--- a/src/calibre/gui2/convert/structure_detection.py
+++ b/src/calibre/gui2/convert/structure_detection.py
@@ -31,7 +31,7 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
self.opt_chapter.set_msg(_('Detect chapters at (XPath expression):'))
self.opt_page_breaks_before.set_msg(_('Insert page breaks before '
'(XPath expression):'))
-
+
def break_cycles(self):
Widget.break_cycles(self)
From 383b15e254c80b596d8455983d2a51db7c367511 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 16:40:29 -0700
Subject: [PATCH 03/61] Cleanup S&R preferences widget
---
src/calibre/gui2/convert/bulk.py | 2 +-
.../gui2/convert/search_and_replace.py | 18 ++++----
.../gui2/convert/search_and_replace.ui | 44 ++++++++++++++-----
src/calibre/gui2/convert/single.py | 2 +-
src/calibre/gui2/preferences/conversion.py | 4 +-
src/calibre/manual/regexp.rst | 2 +-
6 files changed, 48 insertions(+), 24 deletions(-)
diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py
index b97ab1a2dc..591ac92b2b 100644
--- a/src/calibre/gui2/convert/bulk.py
+++ b/src/calibre/gui2/convert/bulk.py
@@ -94,7 +94,7 @@ def widget_factory(cls):
if not c: break
self.stack.removeWidget(c)
- widgets = [lf, hw, sr, ps, sd, toc]
+ widgets = [lf, hw, ps, sd, toc, sr]
if output_widget is not None:
widgets.append(output_widget)
for w in widgets:
diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py
index c85e4fe414..88f7a2d4a0 100644
--- a/src/calibre/gui2/convert/search_and_replace.py
+++ b/src/calibre/gui2/convert/search_and_replace.py
@@ -15,6 +15,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
TITLE = _('Search &\nReplace')
HELP = _('Modify the document text and structure using user defined patterns.')
COMMIT_NAME = 'search_and_replace'
+ ICON = I('search.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
@@ -24,19 +25,19 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
)
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)
- self.opt_sr1_search.set_msg(_('Search Regular Expression'))
+ self.opt_sr1_search.set_msg(_('&Search Regular Expression'))
self.opt_sr1_search.set_book_id(book_id)
self.opt_sr1_search.set_db(db)
- self.opt_sr2_search.set_msg(_('Search Regular Expression'))
+ self.opt_sr2_search.set_msg(_('&Search Regular Expression'))
self.opt_sr2_search.set_book_id(book_id)
self.opt_sr2_search.set_db(db)
- self.opt_sr3_search.set_msg(_('Search Regular Expression'))
+ self.opt_sr3_search.set_msg(_('&Search Regular Expression'))
self.opt_sr3_search.set_book_id(book_id)
self.opt_sr3_search.set_db(db)
-
+
def break_cycles(self):
Widget.break_cycles(self)
-
+
self.opt_sr1_search.break_cycles()
self.opt_sr2_search.break_cycles()
self.opt_sr3_search.break_cycles()
@@ -45,10 +46,11 @@ def pre_commit_check(self):
for x in ('sr1_search', 'sr2_search', 'sr3_search'):
x = getattr(self, 'opt_'+x)
try:
- pat = unicode(x.regex)
- re.compile(pat)
+ pat = unicode(x.regex).strip()
+ if pat:
+ re.compile(pat)
except Exception, err:
error_dialog(self, _('Invalid regular expression'),
- _('Invalid regular expression: %s')%err).exec_()
+ _('Invalid regular expression: %s')%err, show=True)
return False
return True
diff --git a/src/calibre/gui2/convert/search_and_replace.ui b/src/calibre/gui2/convert/search_and_replace.ui
index e0e9570f8c..b7447f8feb 100644
--- a/src/calibre/gui2/convert/search_and_replace.ui
+++ b/src/calibre/gui2/convert/search_and_replace.ui
@@ -6,8 +6,8 @@
0
0
- 198
- 350
+ 468
+ 451
@@ -23,7 +23,7 @@
QLayout::SetDefaultConstraint
-
-
+
-
@@ -32,7 +32,7 @@
- 1.
+ First expression
@@ -57,7 +57,10 @@
- Replacement Text
+ &Replacement Text
+
+
+ opt_sr1_replace
@@ -74,7 +77,7 @@
- -
+
-
@@ -83,7 +86,7 @@
- 2.
+ Second Expression
@@ -108,7 +111,10 @@
- Replacement Text
+ &Replacement Text
+
+
+ opt_sr2_replace
@@ -125,7 +131,7 @@
- -
+
-
@@ -134,7 +140,7 @@
- 3.
+ Third expression
@@ -159,7 +165,10 @@
- Replacement Text
+ &Replacement Text
+
+
+ opt_sr3_replace
@@ -176,6 +185,19 @@
+ -
+
+
+ <p>Search and replace uses <i>regular expressions</i>. See the <a href="http://calibre-ebook.com/user_manual/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard buttons below will allow you to test your regular expression against the current input document.
+
+
+ true
+
+
+ true
+
+
+
diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py
index 8826d398f5..da58de545b 100644
--- a/src/calibre/gui2/convert/single.py
+++ b/src/calibre/gui2/convert/single.py
@@ -207,7 +207,7 @@ def widget_factory(cls):
if not c: break
self.stack.removeWidget(c)
- widgets = [self.mw, lf, hw, sr, ps, sd, toc]
+ widgets = [self.mw, lf, hw, ps, sd, toc, sr]
if input_widget is not None:
widgets.append(input_widget)
if output_widget is not None:
diff --git a/src/calibre/gui2/preferences/conversion.py b/src/calibre/gui2/preferences/conversion.py
index 0a8fc375ea..8de9ee1661 100644
--- a/src/calibre/gui2/preferences/conversion.py
+++ b/src/calibre/gui2/preferences/conversion.py
@@ -85,8 +85,8 @@ class CommonOptions(Base):
def load_conversion_widgets(self):
self.conversion_widgets = [LookAndFeelWidget, HeuristicsWidget,
- SearchAndReplaceWidget, PageSetupWidget,
- StructureDetectionWidget, TOCWidget]
+ PageSetupWidget,
+ StructureDetectionWidget, TOCWidget, SearchAndReplaceWidget,]
class InputOptions(Base):
diff --git a/src/calibre/manual/regexp.rst b/src/calibre/manual/regexp.rst
index 5cd9a8b097..c8661cd427 100644
--- a/src/calibre/manual/regexp.rst
+++ b/src/calibre/manual/regexp.rst
@@ -21,7 +21,7 @@ This is, inevitably, going to be somewhat technical- after all, regular expressi
Where in |app| can you use regular expressions?
---------------------------------------------------
-There are a few places |app| uses regular expressions. There's the header/footer removal in conversion options, metadata detection from filenames in the import settings and, since last version, there's the option to use regular expressions to search and replace in metadata of multiple books.
+There are a few places |app| uses regular expressions. There's the Search & Replace in conversion options, metadata detection from filenames in the import settings and Search & Replace when editing the metadata of books in bulk.
What on earth *is* a regular expression?
------------------------------------------------
From 10f4b48d681d05c98697e5c50bf10e5bea93774c Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 16:42:07 -0700
Subject: [PATCH 04/61] ...
---
src/calibre/gui2/convert/search_and_replace.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py
index 88f7a2d4a0..0c7ae56ea0 100644
--- a/src/calibre/gui2/convert/search_and_replace.py
+++ b/src/calibre/gui2/convert/search_and_replace.py
@@ -46,9 +46,8 @@ def pre_commit_check(self):
for x in ('sr1_search', 'sr2_search', 'sr3_search'):
x = getattr(self, 'opt_'+x)
try:
- pat = unicode(x.regex).strip()
- if pat:
- re.compile(pat)
+ pat = unicode(x.regex)
+ re.compile(pat)
except Exception, err:
error_dialog(self, _('Invalid regular expression'),
_('Invalid regular expression: %s')%err, show=True)
From 4ae546bb60bc0d6d2fa2a777f166b3b799b6863e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 16:54:41 -0700
Subject: [PATCH 05/61] Complete review of Heuristics Processing and S&R
conversion options
---
src/calibre/ebooks/txt/output.py | 4 ++--
src/calibre/gui2/convert/txt_output.py | 15 ++++++---------
src/calibre/gui2/convert/xexp_edit.ui | 4 ++--
src/calibre/manual/conversion.rst | 22 +++++++++++-----------
src/calibre/manual/faq.rst | 4 ++--
5 files changed, 23 insertions(+), 26 deletions(-)
diff --git a/src/calibre/ebooks/txt/output.py b/src/calibre/ebooks/txt/output.py
index 4d0d176fe4..29b3d899bc 100644
--- a/src/calibre/ebooks/txt/output.py
+++ b/src/calibre/ebooks/txt/output.py
@@ -51,12 +51,12 @@ class TXTOutput(OutputFormatPlugin):
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Do not remove links within the document. This is only ' \
'useful when paired with the markdown-format option because' \
- 'links are always removed with plain text output.')),
+ ' links are always removed with plain text output.')),
OptionRecommendation(name='keep_image_references',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Do not remove image references within the document. This is only ' \
'useful when paired with the markdown-format option because' \
- 'image references are always removed with plain text output.')),
+ ' image references are always removed with plain text output.')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
diff --git a/src/calibre/gui2/convert/txt_output.py b/src/calibre/gui2/convert/txt_output.py
index a16dd68014..0e6a6b9574 100644
--- a/src/calibre/gui2/convert/txt_output.py
+++ b/src/calibre/gui2/convert/txt_output.py
@@ -23,9 +23,9 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
['newline', 'max_line_length', 'force_max_line_length',
'inline_toc', 'markdown_format', 'keep_links', 'keep_image_references',
'txt_output_encoding'])
- self.db, self.book_id = db, book_id
+ self.db, self.book_id = db, book_id
for x in get_option('newline').option.choices:
- self.opt_newline.addItem(x)
+ self.opt_newline.addItem(x)
self.initialize_options(get_option, get_help, db, book_id)
self.opt_markdown_format.stateChanged.connect(self.enable_markdown_format)
@@ -33,17 +33,14 @@ def __init__(self, parent, get_option, get_help, db=None, book_id=None):
def break_cycles(self):
Widget.break_cycles(self)
-
+
try:
self.opt_markdown_format.stateChanged.disconnect()
except:
pass
-
+
def enable_markdown_format(self, state):
- if state == Qt.Checked:
- state = True
- else:
- state = False
+ state = state == Qt.Checked
self.opt_keep_links.setEnabled(state)
self.opt_keep_image_references.setEnabled(state)
-
\ No newline at end of file
+
diff --git a/src/calibre/gui2/convert/xexp_edit.ui b/src/calibre/gui2/convert/xexp_edit.ui
index 4b26eb8dcf..18b7c39b52 100644
--- a/src/calibre/gui2/convert/xexp_edit.ui
+++ b/src/calibre/gui2/convert/xexp_edit.ui
@@ -6,7 +6,7 @@
0
0
- 434
+ 430
74
@@ -59,7 +59,7 @@
...
-
+
:/images/wizard.png:/images/wizard.png
diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst
index 2bc5687262..de27a5f5bb 100644
--- a/src/calibre/manual/conversion.rst
+++ b/src/calibre/manual/conversion.rst
@@ -266,14 +266,14 @@ from bad formatting. Because these functions rely on common patterns, be aware t
option may lead to worse results, so use with care. As an example, several of these options will
remove all non-breaking-space entities.
-:guilabel:`Preprocess input`
- This option activates various activates |app|'s Heuristic Processing stage of the conversion pipeline.
+:guilabel:`Enable heuristic processing`
+ This option activates |app|'s Heuristic Processing stage of the conversion pipeline.
This must be enabled in order for various sub-functions to be applied
:guilabel:`Unwrap lines`
Enabling this option will cause |app| to attempt to detect and correct hard line breaks that exist
- within a document using punctuation clues and line length. |app| will first attempt to detect whether
- hard line breaks exist, if they do not appear to exist |app| will not attempt to unwrap lines. The
+ within a document using punctuation clues and line length. |app| will first attempt to detect whether
+ hard line breaks exist, if they do not appear to exist |app| will not attempt to unwrap lines. The
line-unwrap factor can be reduced if you want to 'force' |app| to unwrap lines.
:guilabel:`Line-unwrap factor`
@@ -284,21 +284,21 @@ remove all non-breaking-space entities.
:guilabel:`Detect and markup unformatted chapter headings and sub headings`
If your document does not have Chapter Markers and titles formatted differently from the rest of the text,
- |app| can use this option to attempt detection them and surround them with heading tags. <h2> tags are used
- for chapter headings; <h3> tags are used for any titles that are detected.
+ |app| can use this option to attempt detection them and surround them with heading tags. tags are used
+ for chapter headings; tags are used for any titles that are detected.
This function will not create a TOC, but in many cases it will cause |app|'s default chapter detection settings
- to correctly detect chapters and build a TOC. Adjust the Xpath under Structure Detection if a TOC is not automatically
+ to correctly detect chapters and build a TOC. Adjust the XPath under Structure Detection if a TOC is not automatically
created. If there are no other headings used in the document then setting "//h:h2" under Structure Detection would
be the easiest way to create a TOC for the document.
- The inserted headings are not formatted, to apply formatting use the 'extra_css' option under
+ The inserted headings are not formatted, to apply formatting use the :guilabel:`Extra CSS` option under
the Look and Feel conversion settings. For example, to center heading tags, use the following::
h2, h3 { text-align: center }
-:guilabel:`Renumber sequences of <h1> or <h2> tags`
- Some publishers format chapter headings using multiple <h1> or <h2> tags sequentially.
+:guilabel:`Renumber sequences of or tags`
+ Some publishers format chapter headings using multiple or tags sequentially.
|app|'s default conversion settings will cause such titles to be split into two pieces. This option
will re-number the heading tags to prevent splitting.
@@ -345,7 +345,7 @@ specifying a replacement expression.
The search works by using a python regular expression. All matched text is simply removed from
the document or replaced using the replacement pattern. You can learn more about regular expressions and
-their syntax at http://docs.python.org/library/re.html.
+their syntax at :ref:`regexptutorial`.
.. _structure-detection:
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index b473893673..37d18ea329 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -107,10 +107,10 @@ My device is not being detected by |app|?
Follow these steps to find the problem:
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
- * Make sure you are running the latest version of |app|. The latest version can always be downloaded from `http://calibre-ebook.com/download`_.
+ * Make sure you are running the latest version of |app|. The latest version can always be downloaded from `the calibre website `_.
* Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled.
- * If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `http://bugs.calibre-ebook.com`_.
+ * If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker `_.
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 2c8218bc36fbd1941834d3e94446127d09106507 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 17:34:45 -0700
Subject: [PATCH 06/61] ...
---
src/calibre/ebooks/conversion/preprocess.py | 5 +++--
src/calibre/gui2/convert/search_and_replace.py | 2 +-
src/calibre/gui2/convert/single.ui | 6 +++---
src/calibre/manual/regexp.rst | 4 ++--
4 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py
index bbd71ede3a..087d8ed486 100644
--- a/src/calibre/ebooks/conversion/preprocess.py
+++ b/src/calibre/ebooks/conversion/preprocess.py
@@ -7,7 +7,7 @@
import functools, re
-from calibre import entity_to_unicode
+from calibre import entity_to_unicode, as_unicode
XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>')
SVG_NS = 'http://www.w3.org/2000/svg'
@@ -463,7 +463,8 @@ def __call__(self, html, remove_special_chars=None,
replace_txt = ''
rules.insert(0, (search_re, replace_txt))
except Exception as e:
- self.log.error('Failed to parse %s regexp because %s' % (search, e))
+ self.log.error('Failed to parse %r regexp because %s' %
+ (search, as_unicode(e)))
end_rules = []
# delete soft hyphens - moved here so it's executed after header/footer removal
diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py
index 0c7ae56ea0..04a337a4fc 100644
--- a/src/calibre/gui2/convert/search_and_replace.py
+++ b/src/calibre/gui2/convert/search_and_replace.py
@@ -12,7 +12,7 @@
class SearchAndReplaceWidget(Widget, Ui_Form):
- TITLE = _('Search &\nReplace')
+ TITLE = _(u'Search\u00a0&\nReplace')
HELP = _('Modify the document text and structure using user defined patterns.')
COMMIT_NAME = 'search_and_replace'
ICON = I('search.png')
diff --git a/src/calibre/gui2/convert/single.ui b/src/calibre/gui2/convert/single.ui
index ede548d8d7..bb447104d8 100644
--- a/src/calibre/gui2/convert/single.ui
+++ b/src/calibre/gui2/convert/single.ui
@@ -100,7 +100,7 @@
- 20
+ 10
true
@@ -129,8 +129,8 @@
0
0
- 805
- 484
+ 810
+ 494
diff --git a/src/calibre/manual/regexp.rst b/src/calibre/manual/regexp.rst
index c8661cd427..776141b113 100644
--- a/src/calibre/manual/regexp.rst
+++ b/src/calibre/manual/regexp.rst
@@ -94,7 +94,7 @@ I think I'm beginning to understand these regular expressions now... how do I us
Conversions
^^^^^^^^^^^^^^
-Let's begin with the conversion settings, which is really neat. In the structure detection part, you can input a regexp (short for regular expression) that describes the header or footer string that will be removed during the conversion. The neat part is the wizard. Click on the wizard staff and you get a preview of what |app| "sees" during the conversion process. Scroll down to the header or footer you want to remove, select and copy it, paste it into the regexp field on top of the window. If there are variable parts, like page numbers or so, use sets and quantifiers to cover those, and while you're at it, remember to escape special characters, if there are some. Hit the button labeled :guilabel:`Test` and |app| highlights the parts it would remove were you to use the regexp. Once you're satisfied, hit OK and convert. Be careful if your conversion source has tags like this example::
+Let's begin with the conversion settings, which is really neat. In the Search and Replace part, you can input a regexp (short for regular expression) that describes the string that will be replaced during the conversion. The neat part is the wizard. Click on the wizard staff and you get a preview of what |app| "sees" during the conversion process. Scroll down to the string you want to remove, select and copy it, paste it into the regexp field on top of the window. If there are variable parts, like page numbers or so, use sets and quantifiers to cover those, and while you're at it, remember to escape special characters, if there are some. Hit the button labeled :guilabel:`Test` and |app| highlights the parts it would replace were you to use the regexp. Once you're satisfied, hit OK and convert. Be careful if your conversion source has tags like this example::
Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that.
@@ -104,7 +104,7 @@ Let's begin with the conversion settings, which is really neat. In the structure
It had only been two years since Addison v. Clark.
The court case gave us a revised version of what life was
-(shamelessly ripped out of `this thread `_). You'd have to remove some of the tags as well. In this example, I'd recommend beginning with the tag ````, now you have to end with the corresponding closing tag (opening tags are ````, closing tags are ````), which is simply the next ```` in this case. (Refer to a good HTML manual or ask in the forum if you are unclear on this point.) The opening tag can be described using ````, the closing tag using ````, thus we could remove everything between those tags using ``.*?``. But using this expression would be a bad idea, because it removes everything enclosed by - tags (which, by the way, render the enclosed text in bold print), and it's a fair bet that we'll remove portions of the book in this way. Instead, include the beginning of the enclosed string as well, making the regular expression ``\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?`` The ``\s`` with quantifiers are included here instead of explicitly using the spaces as seen in the string to catch any variations of the string that might occur. Remember to check what |app| will remove to make sure you don't remove any portions you want to keep if you test a new expression. If you only check one occurrence, you might miss a mismatch somewhere else in the text. Also note that should you accidentally remove more or fewer tags than you actually wanted to, |app| tries to repair the damaged code after doing the header/footer removal.
+(shamelessly ripped out of `this thread `_). You'd have to remove some of the tags as well. In this example, I'd recommend beginning with the tag ````, now you have to end with the corresponding closing tag (opening tags are ````, closing tags are ````), which is simply the next ```` in this case. (Refer to a good HTML manual or ask in the forum if you are unclear on this point.) The opening tag can be described using ````, the closing tag using ````, thus we could remove everything between those tags using ``.*?``. But using this expression would be a bad idea, because it removes everything enclosed by - tags (which, by the way, render the enclosed text in bold print), and it's a fair bet that we'll remove portions of the book in this way. Instead, include the beginning of the enclosed string as well, making the regular expression ``\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?`` The ``\s`` with quantifiers are included here instead of explicitly using the spaces as seen in the string to catch any variations of the string that might occur. Remember to check what |app| will remove to make sure you don't remove any portions you want to keep if you test a new expression. If you only check one occurrence, you might miss a mismatch somewhere else in the text. Also note that should you accidentally remove more or fewer tags than you actually wanted to, |app| tries to repair the damaged code after doing the removal.
Adding books
^^^^^^^^^^^^^^^^
From efb222c534e7b68f6cf562517d79e56cdf90f20e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 19:59:18 -0700
Subject: [PATCH 07/61] ...
---
resources/recipes/dallas.recipe | 33 ++++++++++++++++++++-------------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/resources/recipes/dallas.recipe b/resources/recipes/dallas.recipe
index 8666fbef30..d46427caa9 100644
--- a/resources/recipes/dallas.recipe
+++ b/resources/recipes/dallas.recipe
@@ -7,22 +7,29 @@ class DallasNews(BasicNewsRecipe):
max_articles_per_feed = 25
no_stylesheets = True
- remove_tags_before = dict(name='h2', attrs={'class':'vitstoryheadline'})
- remove_tags_after = dict(name='div', attrs={'style':'width: 100%; clear: right'})
- remove_tags_after = dict(name='div', attrs={'id':'article_tools_bottom'})
+ use_embedded_content = False
+ remove_tags_before = dict(name='h1')
+ keep_only_tags = {'class':lambda x: x and 'article' in x}
remove_tags = [
- dict(name='iframe'),
- dict(name='div', attrs={'class':'biblockmore'}),
- dict(name='div', attrs={'style':'width: 100%; clear: right'}),
- dict(name='div', attrs={'id':'article_tools_bottom'}),
- #dict(name='ul', attrs={'class':'articleTools'}),
+ {'class':['DMNSocialTools', 'article ', 'article first ', 'article premium']},
]
feeds = [
- ('Latest News', 'http://www.dallasnews.com/newskiosk/rss/dallasnewslatestnews.xml'),
- ('Local News', 'http://www.dallasnews.com/newskiosk/rss/dallasnewslocalnews.xml'),
- ('Nation and World', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsnationworld.xml'),
- ('Politics', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsnationalpolitics.xml'),
- ('Science', 'http://www.dallasnews.com/newskiosk/rss/dallasnewsscience.xml'),
+ ('Local News',
+ 'http://www.dallasnews.com/news/politics/local-politics/?rss'),
+ ('National Politics',
+ 'http://www.dallasnews.com/news/politics/national-politic/?rss'),
+ ('State Politics',
+ 'http://www.dallasnews.com/news/politics/state-politics/?rss'),
+ ('Religion',
+ 'http://www.dallasnews.com/news/religion/?rss'),
+ ('Crime',
+ 'http://www.dallasnews.com/news/crime/headlines/?rss'),
+ ('Celebrity News',
+ 'http://www.dallasnews.com/entertainment/celebrity-news/?rss&listname=TopStories'),
+ ('Nation',
+ 'http://www.dallasnews.com/news/nation-world/nation/?rss'),
+ ('World',
+ 'http://www.dallasnews.com/news/nation-world/world/?rss'),
]
From d6b446de729abc94147b0b49ffaa08d00a32a878 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 20:25:51 -0700
Subject: [PATCH 08/61] ...
---
src/calibre/ebooks/conversion/preprocess.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py
index 087d8ed486..f728bec52b 100644
--- a/src/calibre/ebooks/conversion/preprocess.py
+++ b/src/calibre/ebooks/conversion/preprocess.py
@@ -459,7 +459,7 @@ def __call__(self, html, remove_special_chars=None,
try:
search_re = re.compile(search_pattern)
replace_txt = getattr(self.extra_opts, replace, '')
- if replace_txt == None:
+ if not replace_txt:
replace_txt = ''
rules.insert(0, (search_re, replace_txt))
except Exception as e:
From 9a8f0398be60d46cf97972d373adaf0310b48e64 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 18 Jan 2011 20:36:34 -0700
Subject: [PATCH 09/61] ...
---
src/calibre/gui2/convert/heuristics.ui | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/convert/heuristics.ui b/src/calibre/gui2/convert/heuristics.ui
index 4358512996..6863fcf8e6 100644
--- a/src/calibre/gui2/convert/heuristics.ui
+++ b/src/calibre/gui2/convert/heuristics.ui
@@ -17,11 +17,14 @@
-
- <b>Heuristic processing</b> means that calibre will scan your book for common patterns and fix them. As the name implies, this involves guesswork, which means that it could end up worsening the result of a conversion, if calibre guesses wrong. Therefore, it is disabled by default. Often, if a conversion does not turn out as you expect, turning on heuristics can improve matters.
+ <b>Heuristic processing</b> means that calibre will scan your book for common patterns and fix them. As the name implies, this involves guesswork, which means that it could end up worsening the result of a conversion, if calibre guesses wrong. Therefore, it is disabled by default. Often, if a conversion does not turn out as you expect, turning on heuristics can improve matters. Read more about the various heuristic processing options in the <a href="http://calibre-ebook.com/user_manual/conversion.html#heuristic-processing">User Manual</a>.
true
+
+ true
+
-
From 06cbaca2e6160b83467c16c5737462a4c312816a Mon Sep 17 00:00:00 2001
From: ldolse
Date: Wed, 19 Jan 2011 22:21:11 +0800
Subject: [PATCH 10/61] start at enabling some heuristics options by default
---
src/calibre/ebooks/conversion/cli.py | 1 +
src/calibre/ebooks/conversion/plumber.py | 10 +++++-----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index b5c057b0f9..8cd4f124d5 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -69,6 +69,7 @@ def option_recommendation_to_cli_option(add_option, rec):
opt = rec.option
switches = ['-'+opt.short_switch] if opt.short_switch else []
switches.append('--'+opt.long_switch)
+ flip_switches = ['italicize_common_cases', 'markup_chapter_headings', 'unwrap_lines', 'dehyphenate', 'fix_indents']
attrs = dict(dest=opt.name, help=opt.help,
choices=opt.choices, default=rec.recommended_value)
if isinstance(rec.recommended_value, type(True)):
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 04ee892c19..7dd977cd7b 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -490,19 +490,19 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
'heuristic processing to take place.')),
OptionRecommendation(name='markup_chapter_headings',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Detect unformatted chapter headings and sub headings. Change '
'them to h2 and h3 tags. This setting will not create a TOC, '
'but can be used in conjunction with structure detection to create '
'one.')),
OptionRecommendation(name='italicize_common_cases',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Look for common words and patterns that denote '
'italics and italicize them.')),
OptionRecommendation(name='fix_indents',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Turn indentation created from multiple non-breaking space entities '
'into CSS indents.')),
@@ -515,7 +515,7 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
'be reduced')),
OptionRecommendation(name='unwrap_lines',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Unwrap lines using punctuation and other formatting clues.')),
OptionRecommendation(name='delete_blank_paragraphs',
@@ -530,7 +530,7 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
'horizontal rules.')),
OptionRecommendation(name='dehyphenate',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Analyze hyphenated words throughout the document. The '
'document itself is used as a dictionary to determine whether hyphens '
'should be retained or removed.')),
From 35c319f115656182addca7030340dd9b713adfc1 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 19 Jan 2011 15:00:52 +0000
Subject: [PATCH 11/61] Fix bug in bulk edit CC enum types where if all but the
last are None, the combo box shows the last value and the others cannot be
set.
---
src/calibre/gui2/custom_column_widgets.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index d80909c4bb..58985d1121 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -599,7 +599,7 @@ def get_initial_value(self, book_ids):
value = None
ret_value = None
dialog_shown = False
- for book_id in book_ids:
+ for i,book_id in enumerate(book_ids):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
if val and val not in self.col_metadata['display']['enum_values']:
if not dialog_shown:
@@ -610,7 +610,7 @@ def get_initial_value(self, book_ids):
show=True, show_copy_button=False)
dialog_shown = True
ret_value = ' nochange '
- elif value is not None and value != val:
+ elif (value is not None and value != val) or (val and i != 0):
ret_value = ' nochange '
value = val
if ret_value is None:
From e008ff22b71f1b3b4beddb74e0df3ba45c562f25 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 19 Jan 2011 15:08:30 +0000
Subject: [PATCH 12/61] Fix #8451: Error Communicating with Device when sending
covers.
---
src/calibre/devices/prs505/driver.py | 17 +++++++++++++----
src/calibre/devices/usbms/device.py | 11 +++++++----
src/calibre/utils/filenames.py | 28 +++++++++++++++++++++-------
3 files changed, 41 insertions(+), 15 deletions(-)
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 874fbe4b10..234093a164 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -201,10 +201,13 @@ def sync_booklists(self, booklists, end_session=True):
self._card_b_prefix if idx == 2 \
else self._main_prefix
for book in bl:
- p = os.path.join(prefix, book.lpath)
- self._upload_cover(os.path.dirname(p),
- os.path.splitext(os.path.basename(p))[0],
- book, p)
+ try:
+ p = os.path.join(prefix, book.lpath)
+ self._upload_cover(os.path.dirname(p),
+ os.path.splitext(os.path.basename(p))[0],
+ book, p)
+ except:
+ debug_print('FAILED to upload cover', p)
else:
debug_print('PRS505: NOT uploading covers in sync_booklists')
@@ -222,6 +225,12 @@ def set_plugboards(self, plugboards, pb_func):
self.plugboards = plugboards
self.plugboard_func = pb_func
+ def create_upload_path(self, path, mdata, fname, create_dirs=True):
+ maxlen = 250 - (max(len(CACHE_THUMBNAIL), len(MEDIA_THUMBNAIL)) +
+ len('main_thumbnail.jpg') + 1)
+ return self._create_upload_path(path, mdata, fname,
+ create_dirs=create_dirs, maxlen=maxlen)
+
def upload_cover(self, path, filename, metadata, filepath):
opts = self.settings()
if not opts.extra_customization[self.OPT_UPLOAD_COVERS]:
diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py
index 4711a8eec4..ffe2484b38 100644
--- a/src/calibre/devices/usbms/device.py
+++ b/src/calibre/devices/usbms/device.py
@@ -874,8 +874,12 @@ def get_annotations(self, path_map):
return {}
def create_upload_path(self, path, mdata, fname, create_dirs=True):
+ return self._create_upload_path(path, mdata, fname,
+ create_dirs=create_dirs, maxlen=250)
+
+ def _create_upload_path(self, path, mdata, fname, create_dirs=True,
+ maxlen=None):
path = os.path.abspath(path)
- extra_components = []
special_tag = None
if mdata.tags:
@@ -902,7 +906,7 @@ def create_upload_path(self, path, mdata, fname, create_dirs=True):
app_id = str(getattr(mdata, 'application_id', ''))
# The db id will be in the created filename
extra_components = get_components(template, mdata, fname,
- timefmt=opts.send_timefmt, length=250-len(app_id)-1)
+ timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1)
if not extra_components:
extra_components.append(sanitize(self.filename_callback(fname,
mdata)))
@@ -937,12 +941,11 @@ def remove_trailing_periods(x):
return ans
extra_components = list(map(remove_trailing_periods, extra_components))
- components = shorten_components_to(250 - len(path), extra_components)
+ components = shorten_components_to(maxlen - len(path), extra_components)
components = self.sanitize_path_components(components)
filepath = os.path.join(path, *components)
filedir = os.path.dirname(filepath)
-
if create_dirs and not os.path.exists(filedir):
os.makedirs(filedir)
diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py
index 47ccbe73c2..7225ab44c8 100644
--- a/src/calibre/utils/filenames.py
+++ b/src/calibre/utils/filenames.py
@@ -42,30 +42,44 @@ def supports_long_names(path):
else:
return True
-def shorten_components_to(length, components):
+def shorten_component(s, byWhat):
+ l = len(s)
+ if l < byWhat:
+ return s
+ l = int((l-byWhat)/2)
+ if l <= 0:
+ return s
+ return s[0:l] + s[-l:]
+
+def shorten_components_to(length, components, more_to_take = 0):
filepath = os.sep.join(components)
- extra = len(filepath) - length
+ extra = len(filepath) - (length - more_to_take)
if extra < 1:
return components
- delta = int(ceil(extra/float(len(components))))
- ans = []
+ deltas = []
for x in components:
+ pct = len(x)/float(len(filepath))
+ deltas.append(int(ceil(pct*extra)))
+ ans = []
+
+ for i,x in enumerate(components):
+ delta = deltas[i]
if delta > len(x):
r = x[0] if x is components[-1] else ''
else:
if x is components[-1]:
b, e = os.path.splitext(x)
if e == '.': e = ''
- r = b[:-delta]+e
+ r = shorten_component(b, delta)+e
if r.startswith('.'): r = x[0]+r
else:
- r = x[:-delta]
+ r = shorten_component(x, delta)
r = r.strip()
if not r:
r = x.strip()[0] if x.strip() else 'x'
ans.append(r)
if len(os.sep.join(ans)) > length:
- return shorten_components_to(length, ans)
+ return shorten_components_to(length, components, more_to_take+2)
return ans
def find_executable_in_path(name, path=None):
From 27b83959356754d8c0516d9d1da174acb3b0cf2f Mon Sep 17 00:00:00 2001
From: ldolse
Date: Wed, 19 Jan 2011 23:09:18 +0800
Subject: [PATCH 13/61] document updates
---
src/calibre/manual/conversion.rst | 30 ++++++++++++++++--------------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst
index de27a5f5bb..6ec986f26a 100644
--- a/src/calibre/manual/conversion.rst
+++ b/src/calibre/manual/conversion.rst
@@ -260,11 +260,11 @@ The Output profile also controls the screen size. This will cause, for example,
Heuristic Processing
---------------------
-Heuristic Processing provides a variety of functions which can be used that try to detect and correct
+Heuristic Processing provides a variety of functions which can be used to try and detect and correct
common problems in poorly formatted input documents. Use these functions if your input document suffers
-from bad formatting. Because these functions rely on common patterns, be aware that in some cases an
+from poor formatting. Because these functions rely on common patterns, be aware that in some cases an
option may lead to worse results, so use with care. As an example, several of these options will
-remove all non-breaking-space entities.
+remove all non-breaking-space entities, or may include false positive matches relating to the function.
:guilabel:`Enable heuristic processing`
This option activates |app|'s Heuristic Processing stage of the conversion pipeline.
@@ -283,7 +283,7 @@ remove all non-breaking-space entities.
correction, then this value should be reduced to somewhere between 0.1 and 0.2.
:guilabel:`Detect and markup unformatted chapter headings and sub headings`
- If your document does not have Chapter Markers and titles formatted differently from the rest of the text,
+ If your document does not have chapter headings and titles formatted differently from the rest of the text,
|app| can use this option to attempt detection them and surround them with heading tags.
tags are used
for chapter headings; tags are used for any titles that are detected.
@@ -331,21 +331,23 @@ remove all non-breaking-space entities.
Some documents use a convention of defining text indents using non-breaking space entities. When this option is enabled |app| will
attempt to detect this sort of formatting and convert them to a 3% text indent using css.
-.. search-replace:
+.. _search-replace:
Search & Replace
---------------------
-These options are useful primarily for conversion of PDF documents. Often, the conversion leaves
-behind page headers and footers in the text. These options use regular expressions to try and detect
-the headers and footers and remove them. Remember that they operate on the intermediate XHTML produced
-by the conversion pipeline. There is also a wizard to help you customize the regular expressions for
-your document. These options can also be used for generic search and replace of any content by additionally
-specifying a replacement expression.
+These options are useful primarily for conversion of PDF documents or OCR conversions, though they can
+also be used to fix many document specific problems. As an example, some conversions can leaves behind page
+headers and footers in the text. These options use regular expressions to try and detect headers, footers,
+or other arbitrary text and remove or replace them. Remember that they operate on the intermediate XHTML produced
+by the conversion pipeline. There is a wizard to help you customize the regular expressions for
+your document. Click the magic wand beside the expression box, and click the 'Test' button after composing
+your search expression. Successful matches will be highlighted in Yellow.
-The search works by using a python regular expression. All matched text is simply removed from
-the document or replaced using the replacement pattern. You can learn more about regular expressions and
-their syntax at :ref:`regexptutorial`.
+The search works by using a python regular expression. All matched text is simply removed from
+the document or replaced using the replacement pattern. The replacement pattern is optional, if left blank
+then text matching the search pattern will be deleted from the document. You can learn more about regular expressions
+and their syntax at :ref:`regexptutorial`.
.. _structure-detection:
From 6fb6ecad4e555f559f71c683d6e0a0baa37b792c Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 09:32:35 -0700
Subject: [PATCH 14/61] Update La Vanguardia
---
resources/recipes/lavanguardia.recipe | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/resources/recipes/lavanguardia.recipe b/resources/recipes/lavanguardia.recipe
index 6c89227c64..517daf942e 100644
--- a/resources/recipes/lavanguardia.recipe
+++ b/resources/recipes/lavanguardia.recipe
@@ -20,8 +20,8 @@ class LaVanguardia(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
- delay = 1
- encoding = 'cp1252'
+ delay = 5
+ # encoding = 'cp1252'
language = 'es'
direction = 'ltr'
@@ -35,8 +35,8 @@ class LaVanguardia(BasicNewsRecipe):
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
feeds = [
- (u'Ciudadanos' , u'http://feeds.feedburner.com/lavanguardia/ciudadanos' )
- ,(u'Cultura' , u'http://feeds.feedburner.com/lavanguardia/cultura' )
+ (u'Portada' , u'http://feeds.feedburner.com/lavanguardia/home' )
+ ,(u'Cultura' , u'http://feeds.feedburner.com/lavanguardia/cultura' )
,(u'Deportes' , u'http://feeds.feedburner.com/lavanguardia/deportes' )
,(u'Economia' , u'http://feeds.feedburner.com/lavanguardia/economia' )
,(u'El lector opina' , u'http://feeds.feedburner.com/lavanguardia/lectoropina' )
@@ -45,17 +45,17 @@ class LaVanguardia(BasicNewsRecipe):
,(u'Internet y tecnologia', u'http://feeds.feedburner.com/lavanguardia/internet' )
,(u'Motor' , u'http://feeds.feedburner.com/lavanguardia/motor' )
,(u'Politica' , u'http://feeds.feedburner.com/lavanguardia/politica' )
- ,(u'Sucessos' , u'http://feeds.feedburner.com/lavanguardia/sucesos' )
+ ,(u'Sucesos' , u'http://feeds.feedburner.com/lavanguardia/sucesos' )
]
keep_only_tags = [
- dict(name='div', attrs={'class':'element1_3'})
- ]
+ dict(name='div', attrs={'class':'detalle noticia'})
+ ]
remove_tags = [
dict(name=['object','link','script'])
- ,dict(name='div', attrs={'class':['colC','peu']})
+ ,dict(name='div', attrs={'class':['colC','peu','jstoolbar']})
]
remove_tags_after = [dict(name='div', attrs={'class':'text'})]
@@ -67,4 +67,3 @@ def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup
-
From 1f9007b9a9a1e763c4f2436c3a310ef2647176d5 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 10:54:26 -0700
Subject: [PATCH 15/61] Heuristic processing: Enable individual actions by
default
---
src/calibre/ebooks/conversion/cli.py | 25 ++++++++++++++----------
src/calibre/ebooks/conversion/plumber.py | 16 +++++++--------
2 files changed, 23 insertions(+), 18 deletions(-)
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index b5c057b0f9..ac5acdaf4b 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -42,6 +42,12 @@
For full documentation of the conversion system see
''') + 'http://calibre-ebook.com/user_manual/conversion.html'
+HEURISTIC_OPTIONS = ['markup_chapter_headings',
+ 'italicize_common_cases', 'fix_indents',
+ 'html_unwrap_factor', 'unwrap_lines',
+ 'delete_blank_paragraphs', 'format_scene_breaks',
+ 'dehyphenate', 'renumber_headings']
+
def print_help(parser, log):
help = parser.format_help().encode(preferred_encoding, 'replace')
log(help)
@@ -83,6 +89,8 @@ def option_recommendation_to_cli_option(add_option, rec):
if opt.long_switch == 'verbose':
attrs['action'] = 'count'
attrs.pop('type', '')
+ if opt.name in HEURISTIC_OPTIONS and rec.recommended_value is True:
+ switches = ['--disable-'+opt.long_switch]
add_option(Option(*switches, **attrs))
def add_input_output_options(parser, plumber):
@@ -129,18 +137,15 @@ def add_pipeline_options(parser, plumber):
'asciiize',
]
),
-
+
'HEURISTIC PROCESSING' : (
- _('Modify the document text and structure using common patterns.'),
- [
- 'enable_heuristics', 'markup_chapter_headings',
- 'italicize_common_cases', 'fix_indents',
- 'html_unwrap_factor', 'unwrap_lines',
- 'delete_blank_paragraphs', 'format_scene_breaks',
- 'dehyphenate', 'renumber_headings',
- ]
+ _('Modify the document text and structure using common'
+ ' patterns. Disabled by default. Use %s to enable. '
+ ' Individual actions can be diable with the %s options.')
+ % ('--enable-heuristics', '--disable-*'),
+ ['enable_heuristics'] + HEURISTIC_OPTIONS
),
-
+
'SEARCH AND REPLACE' : (
_('Modify the document text and structure using user defined patterns.'),
[
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 04ee892c19..908ca6a493 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -490,19 +490,19 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
'heuristic processing to take place.')),
OptionRecommendation(name='markup_chapter_headings',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Detect unformatted chapter headings and sub headings. Change '
'them to h2 and h3 tags. This setting will not create a TOC, '
'but can be used in conjunction with structure detection to create '
'one.')),
OptionRecommendation(name='italicize_common_cases',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Look for common words and patterns that denote '
'italics and italicize them.')),
OptionRecommendation(name='fix_indents',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Turn indentation created from multiple non-breaking space entities '
'into CSS indents.')),
@@ -515,28 +515,28 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
'be reduced')),
OptionRecommendation(name='unwrap_lines',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Unwrap lines using punctuation and other formatting clues.')),
OptionRecommendation(name='delete_blank_paragraphs',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Remove empty paragraphs from the document when they exist between '
'every other paragraph')),
OptionRecommendation(name='format_scene_breaks',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Left aligned scene break markers are center aligned. '
'Replace soft scene breaks that use multiple blank lines with'
'horizontal rules.')),
OptionRecommendation(name='dehyphenate',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Analyze hyphenated words throughout the document. The '
'document itself is used as a dictionary to determine whether hyphens '
'should be retained or removed.')),
OptionRecommendation(name='renumber_headings',
- recommended_value=False, level=OptionRecommendation.LOW,
+ recommended_value=True, level=OptionRecommendation.LOW,
help=_('Looks for occurrences of sequential or tags. '
'The tags are renumbered to prevent splitting in the middle '
'of chapter headings.')),
From 8d5cf40c081287202bd6dd9fae254404efd725b4 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 11:02:23 -0700
Subject: [PATCH 16/61] Updated translatable strings
---
src/calibre/ebooks/conversion/cli.py | 2 +-
src/calibre/translations/calibre.pot | 1729 ++++++++++++----------
src/calibre/utils/formatter_functions.py | 2 +-
3 files changed, 964 insertions(+), 769 deletions(-)
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index ac5acdaf4b..33ae61f16a 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -141,7 +141,7 @@ def add_pipeline_options(parser, plumber):
'HEURISTIC PROCESSING' : (
_('Modify the document text and structure using common'
' patterns. Disabled by default. Use %s to enable. '
- ' Individual actions can be diable with the %s options.')
+ ' Individual actions can be disabled with the %s options.')
% ('--enable-heuristics', '--disable-*'),
['enable_heuristics'] + HEURISTIC_OPTIONS
),
diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot
index e0c6caa9b5..dae186fa59 100644
--- a/src/calibre/translations/calibre.pot
+++ b/src/calibre/translations/calibre.pot
@@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.7.40\n"
-"POT-Creation-Date: 2011-01-14 15:09+MST\n"
-"PO-Revision-Date: 2011-01-14 15:09+MST\n"
+"POT-Creation-Date: 2011-01-19 11:02+MST\n"
+"PO-Revision-Date: 2011-01-19 11:02+MST\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@@ -38,8 +38,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/epub/periodical.py:127
#: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:100
#: /home/kovid/work/calibre/src/calibre/ebooks/fb2/input.py:102
-#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:332
-#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:335
+#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:331
+#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:334
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1894
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1896
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/output.py:24
@@ -75,9 +75,9 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:81
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:122
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:156
-#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:651
-#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:866
-#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:868
+#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:661
+#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:876
+#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:878
#: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:49
#: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:51
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:952
@@ -90,7 +90,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/jacket.py:118
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ereader/writer.py:173
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ereader/writer.py:174
-#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/input.py:27
+#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/input.py:26
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/palmdoc/writer.py:29
#: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ztxt/writer.py:27
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:82
@@ -110,8 +110,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:82
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:100
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:101
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:335
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:337
+#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:329
+#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:331
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:364
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:371
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:331
@@ -119,15 +119,15 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:160
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:167
#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:42
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:115
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:140
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:142
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1055
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1058
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:120
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:145
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:147
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1050
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1053
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:145
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:185
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:717
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:724
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:193
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:236
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:245
@@ -135,17 +135,17 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:443
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:965
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1158
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:112
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:112
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:191
#: /home/kovid/work/calibre/src/calibre/library/cli.py:215
#: /home/kovid/work/calibre/src/calibre/library/database.py:914
#: /home/kovid/work/calibre/src/calibre/library/database2.py:400
#: /home/kovid/work/calibre/src/calibre/library/database2.py:412
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1469
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:1570
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2410
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2412
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2543
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1472
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:1573
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2413
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2415
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2546
#: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:229
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:158
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:161
@@ -434,11 +434,11 @@ msgstr ""
msgid "Specify the character encoding of the input document. If set this option will override any encoding declared by the document itself. Particularly useful for documents that do not declare an encoding or that have erroneous encoding declarations."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:246
+#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:234
msgid "Conversion Output"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:260
+#: /home/kovid/work/calibre/src/calibre/customize/conversion.py:248
msgid "If specified, the output plugin will try to create output that is as human readable as possible. May not have any effect for some output plugins."
msgstr ""
@@ -639,7 +639,7 @@ msgstr ""
msgid "Comma separated list of directories to send e-books to on the device. The first one that exists will be used"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:106
+#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:107
msgid "Communicate with S60 phones."
msgstr ""
@@ -703,22 +703,22 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2553
#: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:447
#: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:470
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:883
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:889
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:919
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:886
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:892
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:922
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:264
#: /home/kovid/work/calibre/src/calibre/library/database2.py:219
#: /home/kovid/work/calibre/src/calibre/library/database2.py:232
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2274
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2277
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:150
msgid "News"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2554
#: /home/kovid/work/calibre/src/calibre/gui2/catalog/catalog_epub_mobi.py:65
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:599
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2237
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2255
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:600
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2240
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2258
msgid "Catalog"
msgstr ""
@@ -981,7 +981,7 @@ msgid "The Kobo supports only one collection currently: the \"Im_Reading\" list.
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:446
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:279
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:282
msgid "Not Implemented"
msgstr ""
@@ -994,7 +994,7 @@ msgid "Communicate with the Palm Pre"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/misc.py:37
-msgid "Communicate with the Booq Avant"
+msgid "Communicate with the Bq Avant"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/misc.py:58
@@ -1026,19 +1026,19 @@ msgstr ""
msgid "Communicate with the Acer Lumiread"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/misc.py:211
+#: /home/kovid/work/calibre/src/calibre/devices/misc.py:214
msgid "Communicate with the Aluratek Color"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/misc.py:231
+#: /home/kovid/work/calibre/src/calibre/devices/misc.py:234
msgid "Communicate with the Trekstor"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/misc.py:251
+#: /home/kovid/work/calibre/src/calibre/devices/misc.py:254
msgid "Communicate with the EEE Reader"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/misc.py:271
+#: /home/kovid/work/calibre/src/calibre/devices/misc.py:274
msgid "Communicate with the Nextbook Reader"
msgstr ""
@@ -1147,49 +1147,49 @@ msgstr ""
msgid "Communicate with the Sunstech EB700 reader."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:258
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:261
msgid "Unable to detect the %s disk drive. Try rebooting."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:438
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:441
msgid "Unable to detect the %s mount point. Try rebooting."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:503
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:506
msgid "Unable to detect the %s disk drive."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:596
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:599
msgid "Could not find mount helper: %s."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:608
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:611
msgid "Unable to detect the %s disk drive. Either the device has already been ejected, or your kernel is exporting a deprecated version of SYSFS."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:617
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:620
msgid "Unable to mount main memory (Error code: %d)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:668
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:671
msgid "The main memory of %s is read only. This usually happens because of file system errors."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:816
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:818
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:819
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:821
msgid "The reader has no storage card in this slot."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:820
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:823
msgid "Selected slot: %s is not supported."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:849
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:852
msgid "There is insufficient free space in main memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:851
-#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:853
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:854
+#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:856
msgid "There is insufficient free space on the storage card"
msgstr ""
@@ -1321,47 +1321,56 @@ msgid ""
"For full documentation of the conversion system see\n"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:97
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:105
msgid "INPUT OPTIONS"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:98
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:106
msgid "Options to control the processing of the input %s file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:104
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:112
msgid "OUTPUT OPTIONS"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:105
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:113
msgid "Options to control the processing of the output %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:119
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:127
msgid "Options to control the look and feel of the output"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:135
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:142
+msgid "Modify the document text and structure using common patterns. Disabled by default. Use %s to enable. Individual actions can be disabled with the %s options."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:150
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:16
+msgid "Modify the document text and structure using user defined patterns."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:159
msgid "Control auto-detection of document structure."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:145
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:168
msgid "Control the automatic generation of a Table of Contents. By default, if the source file has a Table of Contents, it will be used in preference to the automatically generated one."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:155
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:178
msgid "Options to set metadata in the output"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:158
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:181
msgid "Options to help with debugging the conversion"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:183
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:207
msgid "List builtin recipes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:256
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/cli.py:280
msgid "Output saved to"
msgstr ""
@@ -1498,140 +1507,180 @@ msgid "Insert the book metadata at the start of the book. This is useful if your
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:381
-msgid "Attempt to detect and correct hard line breaks and other problems in the source file. This may make things worse, so use with care."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:389
-msgid "Scale used to determine the length at which a line should be unwrapped if preprocess is enabled. Valid values are a decimal between 0 and 1. The default is 0.40, just below the median line length. This will unwrap typical books with hard line breaks, but should be reduced if the line length is variable."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:398
msgid "Convert plain quotes, dashes and ellipsis to their typographically correct equivalents. For details, see http://daringfireball.net/projects/smartypants"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:406
-msgid "Use a regular expression to try and remove the header."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:413
-msgid "The regular expression to use to remove the header."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:419
-msgid "Use a regular expression to try and remove the footer."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:426
-msgid "The regular expression to use to remove the footer."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:433
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:390
msgid "Read metadata from the specified OPF file. Metadata read from this file will override any metadata in the source file."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:440
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:397
msgid "Transliterate unicode characters to an ASCII representation. Use with care because this will replace unicode characters with ASCII. For instance it will replace \"%s\" with \"Mikhail Gorbachiov\". Also, note that in cases where there are multiple representations of a character (characters shared by Chinese and Japanese for instance) the representation used by the largest number of people will be used (Chinese in the previous example)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:455
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:412
msgid "Preserve ligatures present in the input document. A ligature is a special rendering of a pair of characters like ff, fi, fl et cetera. Most readers do not have support for ligatures in their default fonts, so they are unlikely to render correctly. By default, calibre will turn a ligature into the corresponding pair of normal characters. This option will preserve them instead."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:467
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:424
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:38
msgid "Set the title."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:471
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:428
msgid "Set the authors. Multiple authors should be separated by ampersands."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:476
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:433
msgid "The version of the title to be used for sorting. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:480
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:437
msgid "String to be used when sorting by author. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:484
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:441
msgid "Set the cover to the specified file or URL"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:488
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:445
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:54
msgid "Set the ebook description."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:492
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:449
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:56
msgid "Set the ebook publisher."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:496
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:453
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:60
msgid "Set the series this ebook belongs to."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:500
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:457
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:62
msgid "Set the index of the book in this series."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:504
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:461
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:64
msgid "Set the rating. Should be a number between 1 and 5."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:508
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:465
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:66
msgid "Set the ISBN of the book."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:512
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:469
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:68
msgid "Set the tags for the book. Should be a comma separated list."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:516
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:473
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:70
msgid "Set the book producer."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:520
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:477
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:72
msgid "Set the language."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:524
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:481
msgid "Set the publication date."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:528
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:485
msgid "Set the book timestamp (used by the date column in calibre)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:629
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:489
+msgid "Enable heuristic processing. This option must be set for any heuristic processing to take place."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:494
+msgid "Detect unformatted chapter headings and sub headings. Change them to h2 and h3 tags. This setting will not create a TOC, but can be used in conjunction with structure detection to create one."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:501
+msgid "Look for common words and patterns that denote italics and italicize them."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:506
+msgid "Turn indentation created from multiple non-breaking space entities into CSS indents."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:511
+msgid "Scale used to determine the length at which a line should be unwrapped. Valid values are a decimal between 0 and 1. The default is 0.4, just below the median line length. If only a few lines in the document require unwrapping this value should be reduced"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:519
+msgid "Unwrap lines using punctuation and other formatting clues."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:523
+msgid "Remove empty paragraphs from the document when they exist between every other paragraph"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:528
+msgid "Left aligned scene break markers are center aligned. Replace soft scene breaks that use multiple blank lines withhorizontal rules."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:534
+msgid "Analyze hyphenated words throughout the document. The document itself is used as a dictionary to determine whether hyphens should be retained or removed."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:540
+msgid "Looks for occurrences of sequential or tags. The tags are renumbered to prevent splitting in the middle of chapter headings."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:546
+msgid "Search pattern (regular expression) to be replaced with sr1-replace."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:551
+msgid "Replacement to replace the text found with sr1-search."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:555
+msgid "Search pattern (regular expression) to be replaced with sr2-replace."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:560
+msgid "Replacement to replace the text found with sr2-search."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:564
+msgid "Search pattern (regular expression) to be replaced with sr3-replace."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:569
+msgid "Replacement to replace the text found with sr3-search."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:669
msgid "Could not find an ebook inside the archive"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:687
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:727
msgid "Values of series index and rating must be numbers. Ignoring"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:694
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:734
msgid "Failed to parse date/time"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:849
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:889
msgid "Converting input to HTML..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:877
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:916
msgid "Running transforms on ebook..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:964
+#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:1003
msgid "Creating"
msgstr ""
@@ -1717,15 +1766,15 @@ msgstr ""
msgid "Specify the sectionization of elements. A value of \"nothing\" turns the book into a single section. A value of \"files\" turns each file into a separate section; use this if your device is having trouble. A value of \"Table of Contents\" turns the entries in the Table of Contents into titles and creates sections; if it fails, adjust the \"Structure Detection\" and/or \"Table of Contents\" settings (turn on \"Force use of auto-generated Table of Contents)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:249
+#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:248
msgid "Traverse links in HTML files breadth first. Normally, they are traversed depth first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:256
+#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:255
msgid "Maximum levels of recursion when following links in HTML files. Must be non-negative. 0 implies that no links in the root HTML file are followed. Default is %default."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:265
+#: /home/kovid/work/calibre/src/calibre/ebooks/html/input.py:264
msgid "Normally this input plugin re-arranges all the input files into a standard folder hierarchy. Only use this option if you know what you are doing as it can result in various nasty side effects in the rest of of the conversion pipeline."
msgstr ""
@@ -2084,7 +2133,6 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:622
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:40
#: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:214
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:189
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:114
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:79
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:383
@@ -2600,7 +2648,7 @@ msgid "%s format books are not supported"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/cover.py:98
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:171
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:172
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:218
msgid "Book %s of %s"
msgstr ""
@@ -2879,17 +2927,12 @@ msgstr ""
msgid "Table of Contents:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:290
+#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:288
msgid ""
"This RTF file has a feature calibre does not support. Convert it to HTML first and then try it.\n"
"%s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/ebooks/rtf2xml/delete_info.py:203
-msgid ""
-"No action in dictionary state is \"%s\" \n"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/ebooks/rtf2xml/hex_2_utf8.py:296
msgid "error no state found in hex_2_utf8"
msgstr ""
@@ -2975,11 +3018,11 @@ msgid "Produce Markdown formatted text."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/txt/output.py:52
-msgid "Do not remove links within the document. This is only useful when paired with the markdown-format option becauselinks are always removed with plain text output."
+msgid "Do not remove links within the document. This is only useful when paired with the markdown-format option because links are always removed with plain text output."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/txt/output.py:57
-msgid "Do not remove image references within the document. This is only useful when paired with the markdown-format option becauseimage references are always removed with plain text output."
+msgid "Do not remove image references within the document. This is only useful when paired with the markdown-format option because image references are always removed with plain text output."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:70
@@ -3096,7 +3139,7 @@ msgid "Disable UI animations"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:188
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:509
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:520
msgid "Copied"
msgstr ""
@@ -3153,99 +3196,99 @@ msgstr ""
msgid "How many empty books should be added?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:159
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:220
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:162
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:223
msgid "Uploading books to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:176
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:179
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:306
msgid "Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:177
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:180
msgid "EPUB Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:178
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:181
msgid "LRF Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:179
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:182
msgid "HTML Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:180
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:183
msgid "LIT Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:181
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:184
msgid "MOBI Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:182
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:185
msgid "Topaz books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:183
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:186
msgid "Text books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:184
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:187
msgid "PDF Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:185
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:188
msgid "SNB Books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:186
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:189
msgid "Comics"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:187
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:190
msgid "Archives"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:191
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:194
msgid "Supported books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:230
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:233
msgid "Merged some books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:231
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:234
msgid "Some duplicates were found and merged into the following existing books:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:240
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:243
msgid "Failed to read metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:241
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:244
msgid "Failed to read metadata from the following"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:262
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:267
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:286
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:265
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:270
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:289
msgid "Add to library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:267
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:270
#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:116
#: /home/kovid/work/calibre/src/calibre/gui2/actions/tweak_epub.py:28
#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:85
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:126
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:131
msgid "No book selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:280
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:283
msgid "The following books are virtual and cannot be added to the calibre library:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:286
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:289
msgid "No book files found"
msgstr ""
@@ -3258,7 +3301,7 @@ msgid "Add books to your calibre library from the connected device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/annotate.py:20
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:550
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:544
msgid "Fetch annotations (experimental)"
msgstr ""
@@ -3318,33 +3361,22 @@ msgid "Create catalog of books in your calibre library"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:31
-msgid "No books selected to generate catalog for"
+msgid "No books selected for catalog generation"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:54
msgid "Generating %s catalog..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:59
-#: /home/kovid/work/calibre/src/calibre/gui2/add.py:251
-msgid "No books found"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:60
-msgid ""
-"No books to catalog\n"
-"Check job details"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:70
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:78
msgid "Catalog generated."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:73
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:81
msgid "Export Catalog Directory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:74
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/catalog.py:82
msgid "Select destination for %s.%s"
msgstr ""
@@ -3353,8 +3385,9 @@ msgid "Checking database integrity"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:128
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:600
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:594
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/misc.py:41
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:201
#: /home/kovid/work/calibre/src/calibre/utils/ipc/job.py:54
msgid "Error"
msgstr ""
@@ -3514,7 +3547,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:416
#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:167
#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:101
-#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:781
+#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:780
msgid "Not allowed"
msgstr ""
@@ -3543,6 +3576,7 @@ msgid "Bulk convert"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/convert.py:86
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:488
msgid "Cannot convert"
msgstr ""
@@ -3588,9 +3622,9 @@ msgid "Could not copy books: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:150
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:680
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:813
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:190
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:674
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:854
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:190
msgid "Failed"
msgstr ""
@@ -3671,14 +3705,14 @@ msgid "Main memory"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:176
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:475
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:484
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:469
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:478
msgid "Storage Card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/delete.py:177
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:477
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:486
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:471
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:480
msgid "Storage Card B"
msgstr ""
@@ -3827,7 +3861,7 @@ msgid "covers"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:101
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:224
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:224
msgid "metadata"
msgstr ""
@@ -3902,7 +3936,7 @@ msgid "Move to next highlighted match"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/next_match.py:13
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:340
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:376
msgid "N"
msgstr ""
@@ -4116,36 +4150,41 @@ msgid "View specific format"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:85
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:165
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:170
msgid "Cannot view"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:96
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:98
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:112
+msgid "Format unavailable"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:99
+msgid "Selected books have no formats"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/convert/regex_builder.py:77
msgid "Choose the format to view"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:107
-msgid "Format unavailable"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:108
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:113
msgid "Not all the selected books were available in the %s format. You should convert them first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:115
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:120
msgid "Multiple Books Selected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:116
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:121
msgid "You are attempting to open %d books. Opening too many books at once can be slow and have a negative effect on the responsiveness of your computer. Once started the process cannot be stopped until complete. Do you wish to continue?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:130
msgid "Cannot open folder"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:166
+#: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:171
msgid "%s has no available formats."
msgstr ""
@@ -4170,10 +4209,14 @@ msgid "The specified directory could not be processed."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:250
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:829
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:823
msgid "No books"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/add.py:251
+msgid "No books found"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/add.py:315
msgid "Added"
msgstr ""
@@ -4279,18 +4322,20 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:57
#: /home/kovid/work/calibre/src/calibre/gui2/convert/debug_ui.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:143
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:176
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:58
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:162
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:56
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:79
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:80
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library_ui.py:86
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:426
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:428
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:431
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:436
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:450
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:452
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:454
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:485
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:490
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:412
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:417
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:437
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:458
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:460
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:462
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:92
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/saved_search_editor_ui.py:95
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:161
@@ -4407,9 +4452,9 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output.py:20
#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output.py:20
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:17
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_input.py:13
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:18
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:16
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pml_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/rb_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/snb_output.py:15
@@ -4425,8 +4470,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output.py:20
#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output.py:20
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:17
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:18
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:15
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:16
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pml_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/rb_output.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/convert/snb_output.py:15
@@ -4443,9 +4488,10 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/epub_output_ui.py:56
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_input_ui.py:33
#: /home/kovid/work/calibre/src/calibre/gui2/convert/fb2_output_ui.py:38
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:89
#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output_ui.py:120
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:158
#: /home/kovid/work/calibre/src/calibre/gui2/convert/mobi_output_ui.py:74
#: /home/kovid/work/calibre/src/calibre/gui2/convert/page_setup_ui.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_input_ui.py:36
@@ -4454,12 +4500,13 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/convert/pmlz_output_ui.py:46
#: /home/kovid/work/calibre/src/calibre/gui2/convert/rb_output_ui.py:33
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:147
#: /home/kovid/work/calibre/src/calibre/gui2/convert/snb_output_ui.py:42
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:80
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:52
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc_ui.py:67
#: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_input_ui.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_output_ui.py:66
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:55
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:53
#: /home/kovid/work/calibre/src/calibre/gui2/convert/xpath_wizard_ui.py:72
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:77
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_item_ui.py:40
@@ -4916,16 +4963,16 @@ msgstr ""
msgid "HTML Source"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:36
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:38
msgid "For settings that cannot be specified in this dialog, use the values saved in a previous conversion (if they exist) instead of using the defaults specified in the Preferences"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:70
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:72
msgid "Bulk Convert"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:83
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:185
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/bulk.py:87
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:189
msgid "Options specific to the output format."
msgstr ""
@@ -5140,6 +5187,64 @@ msgstr ""
msgid "0.0 pt"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics.py:14
+msgid ""
+"Heuristic\n"
+"Processing"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics.py:15
+msgid "Modify the document text and structure using common patterns."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:90
+msgid "Heuristic processing means that calibre will scan your book for common patterns and fix them. As the name implies, this involves guesswork, which means that it could end up worsening the result of a conversion, if calibre guesses wrong. Therefore, it is disabled by default. Often, if a conversion does not turn out as you expect, turning on heuristics can improve matters. Read more about the various heuristic processing options in the User Manual."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:91
+msgid "Enable &heuristic processing"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:92
+msgid "Heuristic Processing"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:93
+msgid "Unwrap lines"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:94
+msgid "Line &un-wrap factor :"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:95
+msgid "Detect and markup unformatted chapter headings and sub headings"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:96
+msgid "Renumber sequences of or tags to prevent splitting"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:97
+msgid "Delete blank lines between paragraphs"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:98
+msgid "Ensure scene breaks are consistently formatted"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:99
+msgid "Remove unnecessary hyphens"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:100
+msgid "Italicize common words and patterns"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/heuristics_ui.py:101
+msgid "Replace entity indents with CSS indents"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel.py:16
msgid "Look & Feel"
msgstr ""
@@ -5284,124 +5389,124 @@ msgstr ""
msgid "&Monospaced font family:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:45
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:46
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:115
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:200
msgid "Metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:47
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:48
msgid "Set the metadata. The output file will contain as much of this metadata as possible."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:169
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:174
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:171
msgid "Choose cover for "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:176
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:181
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:178
msgid "Cannot read"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:177
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:182
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:179
msgid "You do not have permission to read the file: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:185
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:192
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:190
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:197
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:187
msgid "Error reading file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:186
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:191
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:188
msgid "
There was an error reading from file:
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:193
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:198
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:196
msgid " is not a valid picture"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:172
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:438
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:159
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:446
msgid "Book Cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:173
-msgid "Use cover from &source file"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:174
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:439
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:160
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:447
msgid "Change &cover image:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:175
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:161
msgid "Browse for an image to use as the cover of this book."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:177
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:401
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:163
+msgid "Use cover from &source file"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:164
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:408
msgid "&Title: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:178
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:402
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:165
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409
msgid "Change the title of this book"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:179
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:404
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:405
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:166
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:450
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:420
msgid "&Author(s): "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:180
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:167
msgid "Author So&rt:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:181
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:168
msgid "Change the author(s) of this book. Multiple authors should be separated by a comma"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:182
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:413
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:169
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:460
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:428
msgid "&Publisher: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:183
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:170
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:429
msgid "Ta&gs: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:184
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:415
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:432
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:171
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:462
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:430
msgid "Tags categorize the book. This is particularly useful while searching.
They can be any words or phrases, separated by commas."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:185
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:422
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:415
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:172
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:469
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:433
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/search_ui.py:214
msgid "&Series:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:186
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:187
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:423
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:424
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:416
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:417
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:173
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:174
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:470
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:471
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:434
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:435
msgid "List of known series. You can add new series."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:188
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:418
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata_ui.py:175
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:438
msgid "Book "
msgstr ""
@@ -5493,7 +5598,7 @@ msgstr ""
msgid "Assume print formatting"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:16
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output.py:14
msgid "PDB Output"
msgstr ""
@@ -5526,7 +5631,7 @@ msgstr ""
msgid "No &Images"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:17
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output.py:15
msgid "PDF Output"
msgstr ""
@@ -5583,11 +5688,60 @@ msgstr ""
msgid "Test"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:171
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:15
+msgid ""
+"Search\240&\n"
+"Replace"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:28
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:31
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:34
+msgid "&Search Regular Expression"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:52
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:86
+msgid "Invalid regular expression"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:53
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:87
+msgid "Invalid regular expression: %s"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:148
+msgid "First expression"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:149
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:151
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:153
+msgid "&Replacement Text"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:150
+msgid "Second Expression"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:152
+msgid "Third expression"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:154
+msgid "
Search and replace uses regular expressions. See the regular expressions tutorial to get started with regular expressions. Also clicking the wizard buttons below will allow you to test your regular expression against the current input document."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:173
msgid "Convert"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:196
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/single.py:200
msgid "Options specific to the input format."
msgstr ""
@@ -5626,87 +5780,49 @@ msgstr ""
msgid "Optimize for full-sceen view "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:17
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:15
msgid ""
"Structure\n"
"Detection"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:19
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:17
msgid "Fine tune the detection of chapter headings and other document structure."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:37
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:31
msgid "Detect chapters at (XPath expression):"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:38
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:32
msgid "Insert page breaks before (XPath expression):"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:40
-msgid "Header regular expression:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:43
-msgid "Footer regular expression:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:59
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:86
-msgid "Invalid regular expression"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:60
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:87
-msgid "Invalid regular expression: %s"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:65
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:42
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:39
msgid "Invalid XPath"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:66
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:43
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:40
msgid "The XPath expression %s is invalid."
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:81
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:53
msgid "Chapter &mark:"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:82
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:54
msgid "Remove first &image"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:83
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:55
msgid "Insert &metadata as page at start of book"
msgstr ""
-#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:84
-msgid "Remove F&ooter"
-msgstr ""
-
-#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:85
-msgid "Remove H&eader"
-msgstr ""
-
-#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:86
-msgid "Line &un-wrap factor during preprocess:"
-msgstr ""
-
-#:
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:87
-msgid "&Preprocess input file to possibly improve structure detection"
-msgstr ""
-
#: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:16
msgid ""
"Table of\n"
@@ -5805,7 +5921,7 @@ msgstr ""
msgid "Do not remove image references before processing"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:56
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:54
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:77
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:78
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_ui.py:46
@@ -5816,8 +5932,8 @@ msgstr ""
msgid "TextLabel"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:57
-msgid "Use a wizard to help construct the XPath expression"
+#: /home/kovid/work/calibre/src/calibre/gui2/convert/xexp_edit_ui.py:55
+msgid "Use a wizard to help construct the Regular expression"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/xpath_wizard_ui.py:73
@@ -5903,11 +6019,15 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:167
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:273
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:494
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:302
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:306
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:499
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:500
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:114
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:134
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:210
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:243
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:247
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:235
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:268
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:272
msgid "Undefined"
msgstr ""
@@ -5944,7 +6064,6 @@ msgid "Automatically number books"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:549
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:435
msgid "Force numbers to start with "
msgstr ""
@@ -5969,168 +6088,168 @@ msgstr ""
msgid "No details available."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:191
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:185
msgid "Device no longer connected."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:309
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:303
msgid "Get device information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:320
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:314
msgid "Get list of books on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:330
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:324
msgid "Get annotations from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:342
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:336
msgid "Send metadata to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:347
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:341
msgid "Send collections to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:382
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:376
msgid "Upload %d books to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:397
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:391
msgid "Delete books from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:408
msgid "Download books from device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:424
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:418
msgid "View book on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:458
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:452
msgid "Set default send to device action"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:464
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:458
msgid "Send to main memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:466
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:460
msgid "Send to storage card A"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:468
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:462
msgid "Send to storage card B"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:473
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:482
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:467
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:476
msgid "Main Memory"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:494
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:488
msgid "Send specific format to"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:495
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:489
msgid "Send and delete from library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:538
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:532
msgid "Eject device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:601
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:595
msgid "Error communicating with device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:617
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1105
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:611
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1100
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:297
msgid "No suitable formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:635
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:629
msgid "Select folder to open as device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:686
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:680
msgid "Error talking to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:687
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:681
msgid "There was a temporary error talking to the device. Please unplug and reconnect the device and or reboot."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:730
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:724
msgid "Device: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:732
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:726
msgid " detected."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:830
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:824
msgid "selected to send"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:835
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:829
msgid "Choose format to send to device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:844
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:838
msgid "No device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:845
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:839
msgid "Cannot send: No device is connected"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:848
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:852
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:842
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:846
msgid "No card"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:849
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:853
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:843
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:847
msgid "Cannot send: Device has no storage card"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:899
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:982
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1099
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:893
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:976
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1094
msgid "Auto convert the following books before uploading to the device?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:928
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:922
msgid "Sending catalogs to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1013
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1007
msgid "Sending news to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1066
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1061
msgid "Sending books to device."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1106
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1101
msgid "Could not upload the following books to the device, as no suitable formats were found. Convert the book(s) to a format supported by your device first."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1170
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1165
msgid "No space on device"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1171
+#: /home/kovid/work/calibre/src/calibre/gui2/device.py:1166
msgid "
Cannot upload books to device there is no more free space available "
msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:118
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:388
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:424
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:255
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:61
msgid "Invalid template"
@@ -6138,7 +6257,7 @@ msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:119
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:389
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:425
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:256
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:62
msgid "The template %s is invalid:"
@@ -6151,24 +6270,33 @@ msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:82
-msgid "Use sub directories"
+msgid "If checked, books are placed into sub directories based on their metadata on the device. If unchecked, books are all put into the top level directory."
msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:83
-msgid "Use author sort for author"
+msgid "Use sub directories"
msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:84
+msgid "Use author sort for author"
+msgstr ""
+
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget_ui.py:85
msgid "Save &template:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:48
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:63
msgid "Add books by ISBN"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:49
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:64
+msgid "&Paste from clipboard"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:65
msgid ""
"
Enter a list of ISBNs in the box to the left, one per line. calibre will automatically create entries for books based on the ISBN and download metadata and covers for them.
\n"
"Any invalid ISBNs in the list will be ignored.
\n"
@@ -6176,8 +6304,8 @@ msgid ""
"9788842915232 >> %s
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:53
-msgid "&Paste from clipboard"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_from_isbn_ui.py:69
+msgid "&Tags to set on created book entries:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:80
@@ -6316,7 +6444,7 @@ msgid "No location selected"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:89
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:654
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:665
msgid "Bad location"
msgstr ""
@@ -6380,19 +6508,19 @@ msgid "&Profile:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comments_dialog.py:24
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:23
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:30
msgid "&OK"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comments_dialog.py:25
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:24
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:31
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tweak_epub_ui.py:60
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/main.py:225
msgid "&Cancel"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comments_dialog_ui.py:43
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog_ui.py:43
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog_ui.py:70
msgid "Edit Comments"
msgstr ""
@@ -6474,13 +6602,13 @@ msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:117
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:828
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:837
msgid "Invalid author name"
msgstr ""
#:
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:118
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:829
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:838
msgid "Author names cannot contain & characters."
msgstr ""
@@ -6626,174 +6754,179 @@ msgstr ""
msgid "Working"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:249
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:386
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:256
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:360
msgid "Lower Case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:250
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:385
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:257
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:359
msgid "Upper Case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:251
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:388
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:258
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:362
msgid "Title Case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:252
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:259
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:363
msgid "Capitalize"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:255
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:262
msgid "Character match"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:256
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:263
msgid "Regular Expression"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:259
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:266
msgid "Replace field"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:260
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:267
msgid "Prepend to field"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:261
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:268
msgid "Append to field"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:271
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:278
msgid "Editing meta information for %d books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:300
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:318
msgid "Immediately make all changes without closing the dialog. This operation cannot be canceled or undone"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:339
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:369
msgid "Book %d:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:354
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:384
msgid "You can destroy your library using this feature. Changes are permanent. There is no undo function. You are strongly encouraged to back up your library before proceeding.Search and replace in text fields using character matching or regular expressions. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:362
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:392
msgid "In character mode, the field is searched for the entered search text. The text is replaced by the specified replacement text everywhere it is found in the specified field. After replacement is finished, the text can be changed to upper-case, lower-case, or title-case. If the case-sensitive check box is checked, the search text must match exactly. If it is unchecked, the search text will match both upper- and lower-case letters"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:373
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:403
msgid "In regular expression mode, the search text is an arbitrary python-compatible regular expression. The replacement text can contain backreferences to parenthesized expressions in the pattern. The search is not anchored, and can match and replace multiple times on the same string. The modification functions (lower-case etc) are applied to the matched text, not to the field as a whole. The destination box specifies the field where the result after matching and replacement is to be assigned. You can replace the text in the field, or prepend or append the matched text. See this reference for more information on python's regular expressions, and in particular the 'sub' function."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:428
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:458
msgid "S/R TEMPLATE ERROR"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:548
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:578
msgid "You must specify a destination when source is a composite field"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:651
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:659
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:754
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:681
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:689
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:788
msgid "Search/replace invalid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:652
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:682
msgid "Authors cannot be set to the empty string. Book title %s not processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:660
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:690
msgid "Title cannot be set to the empty string. Book title %s not processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:755
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:789
msgid "Search pattern is invalid: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:799
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:840
msgid ""
"Applying changes to %d books.\n"
"Phase {0} {1}%%."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:403
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:449
msgid "Edit Meta information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:405
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:451
msgid "A&utomatically set author sort"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:406
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:452
+msgid "&Swap title and author"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:453
msgid "Author s&ort: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:407
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:454
msgid "Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:408
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:409
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:455
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:424
msgid "&Rating:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:409
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:410
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:411
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:456
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:457
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:425
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:426
msgid "Rating of this book. 0-5 stars"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:411
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:458
msgid "No change"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:412
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:412
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:459
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:427
msgid " stars"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:461
msgid "Add ta&gs: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:416
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:417
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:433
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:434
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:463
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:464
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:431
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:432
msgid "Open Tag Editor"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:418
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:465
msgid "&Remove tags:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:419
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:466
msgid "Comma separated list of tags to remove from the books. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:420
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:467
msgid "Check this box to remove all tags from the books."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:421
-msgid "Remove all"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:468
+msgid "Remove &all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:425
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:472
msgid "If checked, the series will be cleared"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:426
-msgid "Clear series"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:473
+msgid "&Clear series"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:427
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:474
msgid ""
"If not checked, the series number for the books will be set to 1.\n"
"If checked, selected books will be automatically numbered, in the order\n"
@@ -6801,188 +6934,211 @@ msgid ""
"Book A will have series number 1 and Book B series number 2."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:431
-msgid "Automatically number books in this series"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:478
+msgid "&Automatically number books in this series"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:432
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:479
msgid ""
"Series will normally be renumbered from the highest number in the database\n"
"for that series. Checking this box will tell calibre to start numbering\n"
"from the value in the box"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:436
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:482
+msgid "&Force numbers to start with:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:483
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:440
+msgid "&Date:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:484
+msgid "d MMM yyyy"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:486
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:491
+msgid "&Apply date"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:487
+msgid "&Published:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:489
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:444
+msgid "Clear published date"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:492
msgid "Remove &format:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:437
-msgid "&Swap title and author"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:438
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:493
msgid ""
"Force the title to be in title case. If both this and swap authors are checked,\n"
"title and author are swapped before the title case is set"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:440
-msgid "Change title to title case"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:495
+msgid "Change title to title &case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:441
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:496
msgid ""
"Remove stored conversion settings for the selected books.\n"
"\n"
"Future conversion of these books will use the default settings."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:444
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:499
msgid "Remove &stored conversion settings for the selected books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:445
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:500
msgid "Change &cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:446
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:501
msgid "&Generate default cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:447
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:502
msgid "&Remove cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:448
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:503
msgid "Set from &ebook file(s)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:449
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:457
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:504
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:465
msgid "&Basic metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:450
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:458
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:505
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:466
msgid "&Custom metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:451
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:506
msgid "Search &field:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:452
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:507
msgid "The name of the field that you want to search"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:453
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:508
msgid "Search &mode:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:454
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:509
msgid "Choose whether to use basic text matching or advanced regular expression matching"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:455
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:510
msgid "Te&mplate:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:456
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:511
msgid "Enter a template to be used as the source for the search/replace"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:457
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:512
msgid "&Search for:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:458
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:513
msgid "Enter the what you are looking for, either plain text or a regular expression, depending on the mode"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:459
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:514
msgid "Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:460
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:515
msgid "Cas&e sensitive"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:461
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:516
msgid "&Replace with:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:462
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:517
msgid "The replacement text. The matched search text will be replaced with this string"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:463
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:518
msgid "&Apply function after replace:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:464
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:519
msgid ""
"Specify how the text is to be processed after matching and replacement. In character mode, the entire\n"
"field is processed. In regular expression mode, only the matched text is processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:466
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:521
msgid "&Destination field:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:467
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:522
msgid ""
"The field that the text will be put into after all replacements.\n"
"If blank, the source field is used if the field is modifiable"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:469
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:524
msgid "M&ode:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:470
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:525
msgid "Specify how the text should be copied into the destination."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:471
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:526
msgid ""
"Specifies whether result items should be split into multiple values or\n"
"left as single values. This option has the most effect when the source field is\n"
"not multiple and the destination field is multiple"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:474
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:529
msgid "Split &result"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:475
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:530
msgid "For multiple-valued fields, sho&w"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:476
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:531
msgid "values starting a&t"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:477
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:532
msgid "with values separated b&y"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:478
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:533
msgid "Used when displaying test results to separate values in multiple-valued fields"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:479
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:534
msgid "Test text"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:480
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:535
msgid "Test result"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:481
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:536
msgid "Your test:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:482
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:537
msgid "&Search and replace"
msgstr ""
@@ -7116,174 +7272,170 @@ msgstr ""
msgid "Next"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:673
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:678
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:680
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:685
msgid "This ISBN number is valid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:681
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:688
msgid "This ISBN number is invalid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:757
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:768
msgid "Tags changed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:758
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:769
msgid "You have changed the tags. In order to use the tags editor, you must either discard or apply these changes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:794
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:805
msgid "Timed out"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:795
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:806
msgid "The download of social metadata timed out, the servers are probably busy. Try again later."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:802
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:813
msgid "There were errors"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:803
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:814
msgid "There were errors downloading social metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:837
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:848
msgid "Cannot fetch metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:838
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:849
msgid "You must specify at least one of ISBN, Title, Authors or Publisher"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:933
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:944
msgid "Permission denied"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:934
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:945
msgid "Could not open %s. Is it being used by another program?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:399
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406
msgid "Edit Meta Information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:400
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407
msgid "Meta information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:403
-msgid "Title &sort: "
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:404
-msgid "Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:406
-msgid "Author S&ort: "
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:407
-msgid ""
-"Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.\n"
-"If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:419
-msgid "IS&BN:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:420
-msgid "&Date:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:421
-msgid "dd MMM yyyy"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:422
-msgid "Publishe&d:"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:424
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410
msgid ""
"Automatically create the title sort entry based on the current title entry.\n"
"Using this button to create title sort will change title sort from red to green."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:427
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413
msgid "Swap the author and title"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:429
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:415
msgid ""
"Automatically create the author sort entry based on the current author entry.\n"
"Using this button to create author sort will change author sort from red to green."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:435
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:418
+msgid "Title &sort: "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:419
+msgid "Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:421
+msgid "Author S&ort: "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:422
+msgid ""
+"Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.\n"
+"If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:436
msgid "Remove unused series (Series that have no books)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:437
-msgid "&Fetch metadata from server"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:440
-msgid "&Browse"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:439
+msgid "IS&BN:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:441
-msgid "Remove border (if any) from cover"
+msgid "dd MMM yyyy"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:442
-msgid "T&rim"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:443
-msgid "Reset cover to default"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:444
-msgid "&Remove"
+msgid "Publishe&d:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:445
-msgid "Download co&ver"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:446
-msgid "Generate a default cover based on the title and author"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:447
-msgid "&Generate cover"
+msgid "&Fetch metadata from server"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:448
-msgid "Available Formats"
+msgid "&Browse"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:449
-msgid "Add a new format for this book to the database"
+msgid "Remove border (if any) from cover"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:450
+msgid "T&rim"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:451
-msgid "Remove the selected formats for this book from the database."
+msgid "Reset cover to default"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:452
+msgid "&Remove"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:453
-msgid "Set the cover for the book from the selected format"
+msgid "Download co&ver"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:454
+msgid "Generate a default cover based on the title and author"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:455
-msgid "Update metadata from the metadata in the selected format"
+msgid "&Generate cover"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:456
+msgid "Available Formats"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:457
+msgid "Add a new format for this book to the database"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:459
+msgid "Remove the selected formats for this book from the database."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:461
+msgid "Set the cover for the book from the selected format"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:463
+msgid "Update metadata from the metadata in the selected format"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:464
msgid "&Comments"
msgstr ""
@@ -7749,12 +7901,12 @@ msgid "%s (was %s)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:74
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:818
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:827
msgid "Item is blank"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:75
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:819
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:828
msgid "An item cannot be set to nothing. Delete it instead."
msgstr ""
@@ -7799,6 +7951,19 @@ msgstr ""
msgid "Ctrl+S"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog_ui.py:71
+msgid "Function &name:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog_ui.py:72
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions_ui.py:100
+msgid "&Documentation:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog_ui.py:73
+msgid "Python &code:"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/test_email_ui.py:56
msgid "Test email settings"
msgstr ""
@@ -8151,7 +8316,7 @@ msgstr ""
msgid "created by Kovid Goyal"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/init.py:166
+#: /home/kovid/work/calibre/src/calibre/gui2/init.py:165
msgid "Connected "
msgstr ""
@@ -8248,7 +8413,7 @@ msgid "Show books in the main memory of the device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:67
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:842
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:845
msgid "Card A"
msgstr ""
@@ -8257,7 +8422,7 @@ msgid "Show books in storage card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:69
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:844
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:847
msgid "Card B"
msgstr ""
@@ -8317,11 +8482,11 @@ msgstr ""
msgid "Delete current saved search"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:340
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:376
msgid "Y"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:375
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:411
msgid "Edit template"
msgstr ""
@@ -8410,7 +8575,7 @@ msgstr ""
msgid "Restore default layout"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:782
+#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:781
msgid "Dropping onto a device is not supported. First add the book to the calibre library."
msgstr ""
@@ -8509,7 +8674,7 @@ msgid "Ignore custom plugins, useful if you installed a plugin that is preventin
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:61
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:662
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:673
msgid "Calibre Library"
msgstr ""
@@ -8621,44 +8786,44 @@ msgstr ""
msgid "ERROR: Unhandled exception"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:109
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:109
msgid "Book has neither title nor ISBN"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:138
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:138
msgid "No matches found for this book"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:191
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:191
msgid "Failed to download metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:224
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:224
msgid "cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:225
msgid "Downloaded"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:225
msgid "Failed to get"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:229
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:229
msgid "%s %s for: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:288
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:288
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/behavior.py:162
msgid "Done"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:289
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:289
msgid "Successfully downloaded metadata for %d out of %d books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata.py:291
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download.py:291
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:661
msgid "Details"
msgstr ""
@@ -8818,7 +8983,7 @@ msgstr ""
msgid "Add &custom column"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/conversion.py:37
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/conversion.py:39
msgid "Restore settings to default values. Only settings for the currently selected section are restored."
msgstr ""
@@ -9392,7 +9557,7 @@ msgid "Installing plugins is a security risk. Plugins can contain a virus
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:180
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:231
msgid "Success"
msgstr ""
@@ -9409,30 +9574,34 @@ msgid "%s is not a valid plugin path"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:202
+msgid "Select an actual plugin under %s to customize"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:208
msgid "Plugin cannot be disabled"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:203
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:209
msgid "The plugin: %s cannot be disabled"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:213
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:219
msgid "Plugin not customizable"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:214
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:220
msgid "Plugin: %s does not need customization"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:220
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:226
msgid "Plugin {0} successfully removed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:228
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:234
msgid "Cannot remove builtin plugin"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:229
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:235
msgid " cannot be removed. It is a builtin plugin. Try disabling it instead."
msgstr ""
@@ -9694,39 +9863,44 @@ msgid ""
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:127
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:137
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:144
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:148
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:159
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:133
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:143
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:150
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:154
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:165
msgid "Template functions"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:128
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:134
msgid "You cannot delete a built-in function"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:138
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:144
msgid "Function not defined"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:145
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:151
msgid "Name already used"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:149
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:155
msgid "Argument count must be -1 or greater than zero"
msgstr ""
#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:160
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:166
msgid "Exception while compiling function"
msgstr ""
+#:
+#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions.py:194
+msgid "function source code not available"
+msgstr ""
+
#:
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions_ui.py:96
msgid "&Function:"
@@ -9747,11 +9921,6 @@ msgstr ""
msgid "Set this to -1 if the function takes a variable number of arguments"
msgstr ""
-#:
-#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions_ui.py:100
-msgid "&Documentation:"
-msgstr ""
-
#:
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/template_functions_ui.py:102
msgid "&Delete"
@@ -9866,7 +10035,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:94
#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:271
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:584
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:592
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:277
msgid "Search"
msgstr ""
@@ -10010,19 +10179,19 @@ msgstr ""
msgid "Searches"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:833
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:842
msgid "Duplicate search name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:834
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:843
msgid "The saved search name %s is already used."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1223
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1232
msgid "Find item in tag browser"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1226
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1235
msgid ""
"Search for items. This is a \"contains\" search; items containing the\n"
"text anywhere in the name will be found. You can limit the search\n"
@@ -10032,59 +10201,59 @@ msgid ""
"containing the text \"foo\""
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1235
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1244
msgid "ALT+f"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1239
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1248
msgid "F&ind"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1240
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1249
msgid "Find the first/next matching item"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1247
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1256
msgid "Collapse all categories"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1268
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1277
msgid "No More Matches.
Click Find again to go to first match"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1281
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1290
msgid "Sort by name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1281
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1290
msgid "Sort by popularity"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1282
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1291
msgid "Sort by average rating"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1285
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1294
msgid "Set the sort order for entries in the Tag Browser"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1291
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1300
msgid "Match all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1291
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1300
msgid "Match any"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1296
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1305
msgid "When selecting multiple entries in the Tag Browser match any or all of them"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1300
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1309
msgid "Manage &user categories"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1303
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1312
msgid "Add your own categories to the Tag Browser"
msgstr ""
@@ -10154,50 +10323,50 @@ msgstr ""
msgid "Conversion Error"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:498
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:499
msgid "Recipe Disabled"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:514
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:515
msgid "Failed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:551
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:552
msgid "is the result of the efforts of many volunteers from all over the world. If you find it useful, please consider donating to support its development. Your donation helps keep calibre development going."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:577
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:578
msgid "There are active jobs. Are you sure you want to quit?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:580
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:581
msgid ""
" is communicating with the device!
\n"
" Quitting may cause corruption on the device.
\n"
" Are you sure you want to quit?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:584
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:585
msgid "WARNING: Active jobs"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:656
+#: /home/kovid/work/calibre/src/calibre/gui2/ui.py:657
msgid "will keep running in the system tray. To close it, choose Quit in the context menu of the system tray."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/update.py:53
-msgid "%s has been updated to version %s. See the new features. Visit the download page?"
+msgid "%s has been updated to version %s. See the new features."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/update.py:59
+#: /home/kovid/work/calibre/src/calibre/gui2/update.py:58
msgid "Update available!"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/update.py:64
+#: /home/kovid/work/calibre/src/calibre/gui2/update.py:63
msgid "Show this notification for future updates"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/update.py:69
+#: /home/kovid/work/calibre/src/calibre/gui2/update.py:68
msgid "&Get update"
msgstr ""
@@ -10697,72 +10866,72 @@ msgstr ""
msgid "Paste Image"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:384
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:358
msgid "Change Case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:387
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:361
msgid "Swap Case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:893
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:901
msgid "Drag to resize"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:928
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:936
msgid "Show"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:935
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:943
msgid "Hide"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:972
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:980
msgid "Toggle"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:400
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:411
msgid "If you use the WordPlayer e-book app on your Android phone, you can access your calibre book collection directly on the device. To do this you have to turn on the content server."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:404
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:415
msgid "Remember to leave calibre running as the server only runs as long as calibre is running."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:406
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:417
msgid "You have to add the URL http://myhostname:8080 as your calibre library in WordPlayer. Here myhostname should be the fully qualified hostname or the IP address of the computer calibre is running on."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:483
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:494
msgid "Moving library..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:499
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:500
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:510
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:511
msgid "Failed to move library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:554
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:565
msgid "Invalid database"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:555
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:566
msgid "
An invalid library already exists at %s, delete it before trying to move the existing library.
Error: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:566
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:577
msgid "Could not move library"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:641
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:652
msgid "Select location for books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:655
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:666
msgid "You must choose an empty folder for the calibre library. %s is not empty."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:729
+#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:740
msgid "welcome wizard"
msgstr ""
@@ -11039,7 +11208,7 @@ msgstr ""
msgid "empty"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:53
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:54
msgid ""
"The fields to output when cataloging books in the database. Should be a comma-separated list of fields.\n"
"Available fields: %s,\n"
@@ -11048,7 +11217,7 @@ msgid ""
"Applies to: CSV, XML output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:64
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:65
msgid ""
"Output field to sort on.\n"
"Available fields: author_sort, id, rating, size, timestamp, title.\n"
@@ -11056,7 +11225,7 @@ msgid ""
"Applies to: CSV, XML output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:231
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:232
msgid ""
"The fields to output when cataloging books in the database. Should be a comma-separated list of fields.\n"
"Available fields: %s.\n"
@@ -11064,7 +11233,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:241
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:242
msgid ""
"Output field to sort on.\n"
"Available fields: author_sort, id, rating, size, timestamp, title.\n"
@@ -11072,7 +11241,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:250
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:251
msgid ""
"Create a citation for BibTeX entries.\n"
"Boolean value: True, False\n"
@@ -11080,7 +11249,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:259
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:260
msgid ""
"The template for citation creation from database fields.\n"
" Should be a template with {} enclosed fields.\n"
@@ -11089,7 +11258,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:269
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:270
msgid ""
"BibTeX file encoding output.\n"
"Available types: utf8, cp1252, ascii.\n"
@@ -11097,7 +11266,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:278
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:279
msgid ""
"BibTeX file encoding flag.\n"
"Available types: strict, replace, ignore, backslashreplace.\n"
@@ -11105,7 +11274,7 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:287
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:288
msgid ""
"Entry type for BibTeX catalog.\n"
"Available types: book, misc, mixed.\n"
@@ -11113,89 +11282,89 @@ msgid ""
"Applies to: BIBTEX output format"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:572
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:573
msgid ""
"Title of generated catalog used as title in metadata.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:579
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:580
msgid ""
"Save the output from different stages of the conversion pipeline to the specified directory. Useful if you are unsure at which stage of the conversion process a bug is occurring.\n"
"Default: '%default'None\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:589
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:590
msgid ""
"field:pattern specifying custom field/contents indicating book should be excluded.\n"
"Default: '%default'\n"
"Applies to ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:596
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:597
msgid ""
"Regex describing tags to exclude as genres.\n"
"Default: '%default' excludes bracketed tags, e.g. '[]'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:602
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:603
msgid ""
"Comma-separated list of tag words indicating book should be excluded from output.For example: 'skip' will match 'skip this book' and 'Skip will like this'.Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:610
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:611
msgid ""
"Include 'Authors' section in catalog.This switch is ignored - Books By Author section is always generated.Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:618
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:619
msgid ""
"Include book descriptions in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:625
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:626
msgid ""
"Include 'Genres' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:632
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:633
msgid ""
"Include 'Titles' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:639
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:640
msgid ""
"Include 'Series' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:646
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:647
msgid ""
"Include 'Recently Added' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:653
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:654
msgid ""
"Custom field containing note text to insert in Description header.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:660
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:661
msgid ""
":[before|after]:[True|False] specifying:\n"
" Custom field containing notes to merge with Comments\n"
@@ -11205,21 +11374,21 @@ msgid ""
"Applies to ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:670
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:671
msgid ""
"Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:677
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:678
msgid ""
"field:pattern indicating book has been read.\n"
"Default: '%default'\n"
"Applies to ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:683
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:684
msgid ""
"Size hint (in inches) for book covers in catalog.\n"
"Range: 1.0 - 2.0\n"
@@ -11227,22 +11396,32 @@ msgid ""
"Applies to ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:691
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:692
msgid ""
"Tag indicating book to be displayed as wishlist item.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/catalog.py:1613
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:1438
msgid ""
"\n"
+"Inconsistent Author Sort values for Author '{0}':\n"
+"'{1}' <> '{2}',\n"
+"unable to build catalog.\n"
"\n"
-"*** Metadata error ***\n"
-"Inconsistent Author Sort values for Author '{0}', unable to continue building catalog.\n"
"Select all books by '{0}', apply correct Author Sort value in Edit Metadata dialog,\n"
"then rebuild the catalog.\n"
-"*** Terminating catalog generation ***\n"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:1640
+msgid ""
+"No books found to catalog.\n"
+"Check 'Excluded books' criteria in E-book options.\n"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/library/catalog.py:1642
+msgid "No books available to include in catalog"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/check_library.py:26
@@ -11319,7 +11498,7 @@ msgid "Filter the results by the search query. For the format of the search quer
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/cli.py:143
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1045
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1042
msgid "The maximum width of a single line in the output. Defaults to detecting screen size."
msgstr ""
@@ -11535,7 +11714,7 @@ msgstr ""
msgid "Error: You must specify a catalog output file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:728
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:725
msgid ""
"\n"
" %prog set_custom [options] column id value\n"
@@ -11547,15 +11726,15 @@ msgid ""
" "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:739
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:736
msgid "If the column stores multiple values, append the specified values to the existing ones, instead of replacing them."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:750
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:747
msgid "Error: You must specify a field name, id and value"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:769
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:766
msgid ""
"\n"
" %prog custom_columns [options]\n"
@@ -11564,19 +11743,19 @@ msgid ""
" "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:776
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:773
msgid "Show details for each column."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:788
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:785
msgid "You will lose all data in the column: %r. Are you sure (y/n)? "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:790
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:787
msgid "y"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:796
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:793
msgid ""
"\n"
" %prog remove_custom_column [options] label\n"
@@ -11586,15 +11765,15 @@ msgid ""
" "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:804
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:801
msgid "Do not ask for confirmation"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:814
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:811
msgid "Error: You must specify a column label"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:824
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:821
msgid ""
"\n"
" %prog saved_searches [options] list\n"
@@ -11607,73 +11786,73 @@ msgid ""
" "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:842
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:839
msgid "Error: You must specify an action (add|remove|list)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:850
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:847
msgid "Name:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:851
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:848
msgid "Search string:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:857
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:854
msgid "Error: You must specify a name and a search string"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:860
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:857
msgid "added"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:865
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:862
msgid "Error: You must specify a name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:868
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:865
msgid "removed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:872
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:869
msgid "Error: Action %s not recognized, must be one of: (add|remove|list)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:880
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:877
msgid ""
"%prog check_library [options]\n"
"\n"
"Perform some checks on the filesystem representing a library. Reports are {0}\n"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:887
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1037
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:884
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1034
msgid "Output in CSV"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:890
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:887
msgid ""
"Comma-separated list of reports.\n"
"Default: all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:894
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:891
msgid ""
"Comma-separated list of extensions to ignore.\n"
"Default: all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:898
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:895
msgid ""
"Comma-separated list of names to ignore.\n"
"Default: all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:928
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:925
msgid "Unknown report check"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:961
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:958
msgid ""
"%prog restore_database [options]\n"
"\n"
@@ -11688,15 +11867,15 @@ msgid ""
" "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:976
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:973
msgid "Really do the recovery. The command will not run unless this option is specified."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:989
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:986
msgid "You must provide the %s option to do a recovery"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1026
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1023
msgid ""
"%prog list_categories [options]\n"
"\n"
@@ -11704,29 +11883,29 @@ msgid ""
"information is the equivalent of what is shown in the tags pane.\n"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1034
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1031
msgid "Output only the number of items in a category instead of the counts per item within the category"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1039
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1036
msgid "The character to put around the category value in CSV mode. Default is quotes (\")."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1042
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1039
msgid ""
"Comma-separated list of category lookup names.\n"
"Default: all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1048
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1045
msgid "The string used to separate fields in CSV mode. Default is a comma."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1086
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1083
msgid "CATEGORY ITEMS"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/cli.py:1155
+#: /home/kovid/work/calibre/src/calibre/library/cli.py:1152
msgid ""
"%%prog command [options] [arguments]\n"
"\n"
@@ -11750,31 +11929,31 @@ msgstr ""
msgid "%sAverage rating is %3.1f"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:840
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:843
msgid "Main"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2569
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2572
msgid "Migrating old database to ebook library in %s
"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2598
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2601
msgid "Copying %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2615
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2618
msgid "Compacting database"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2740
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2743
msgid "Checking SQL integrity..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2778
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2781
msgid "Checking for missing files."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/library/database2.py:2806
+#: /home/kovid/work/calibre/src/calibre/library/database2.py:2809
msgid "Checked id"
msgstr ""
@@ -12228,7 +12407,11 @@ msgstr ""
msgid "format: type {0} requires a decimal (float) value, got {1}"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter.py:341
+#: /home/kovid/work/calibre/src/calibre/utils/formatter.py:296
+msgid "%s: unknown function"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/utils/formatter.py:343
msgid "No such variable "
msgstr ""
@@ -12236,115 +12419,119 @@ msgstr ""
msgid "No documentation provided"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:95
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:78
+msgid "Exception "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:96
msgid "strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x and y as strings. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:110
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:111
msgid "cmp(x, y, lt, eq, gt) -- compares x and y after converting both to numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:125
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:126
msgid "strcat(a, b, ...) -- can take any number of arguments. Returns a string formed by concatenating all the arguments"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:138
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:139
msgid "add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:148
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:149
msgid "subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:158
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:159
msgid "multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:168
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:169
msgid "divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:178
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:179
msgid "template(x) -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the { and } characters are special, you must use [[ for the { character and ]] for the } character; they are converted automatically. For example, template('[[title_sort]]') will evaluate the template {title_sort} and return its value."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:193
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:194
msgid "eval(template) -- evaluates the template, passing the local variables (those 'assign'ed to) instead of the book metadata. This permits using the template processor to construct complex results from local variables."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:206
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:207
msgid "assign(id, val) -- assigns val to id, then returns val. id must be an identifier, not an expression"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:216
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:217
msgid "print(a, b, ...) -- prints the arguments to standard output. Unless you start calibre from the command line (calibre-debug -g), the output will go to a black hole."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:227
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:228
msgid "field(name) -- returns the metadata field named by name"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:235
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:236
msgid "substr(str, start, end) -- returns the start'th through the end'th characters of str. The first character in str is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, substr('12345', 1, 0) returns '2345', and substr('12345', 1, -1) returns '234'."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:248
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:249
msgid "lookup(val, pattern, field, pattern, field, ..., else_field) -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:263
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:264
msgid "lookup requires either 2 or an odd number of arguments"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:275
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:276
msgid "test(val, text if not empty, text if empty) -- return `text if not empty` if the field is not empty, otherwise return `text if empty`"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:287
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:288
msgid "contains(val, pattern, text if match, text if not match) -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:302
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:303
msgid "switch(val, pattern, value, pattern, value, ..., else_value) -- for each `pattern, value` pair, checks if the field matches the regular expression `pattern` and if so, returns that `value`. If no pattern matches, then else_value is returned. You can have as many `pattern, value` pairs as you want"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:310
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:311
msgid "switch requires an odd number of arguments"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:322
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:323
msgid "re(val, pattern, replacement) -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of calibre, these are python-compatible regular expressions"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:333
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:334
msgid "ifempty(val, text if empty) -- return val if val is not empty, otherwise return `text if empty`"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:345
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:346
msgid "shorten(val, left chars, middle text, right chars) -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use {title:shorten(9,-,5)}, the result will be `Ancient E-nhoe`. If the field's length is less than left chars + right chars + the length of `middle text`, then the field will be used intact. For example, the title `The Dome` would not be changed."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:370
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:371
msgid "count(val, separator) -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: {tags:count(,)}, {authors:count(&)}"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:381
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:382
msgid "list_item(val, index, separator) -- interpret the value as a list of items separated by `separator`, returning the `index`th item. The first item is number zero. The last item can be returned using `list_item(-1,separator)`. If the item is not in the list, then the empty value is returned. The separator has the same meaning as in the count function."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:401
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:402
msgid "uppercase(val) -- return value of the field in upper case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:409
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:410
msgid "lowercase(val) -- return value of the field in lower case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:417
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:418
msgid "titlecase(val) -- return value of the field in title case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:425
+#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:426
msgid "capitalize(val) -- return value of the field capitalized"
msgstr ""
@@ -12574,66 +12761,74 @@ msgid "\tFailed links:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:842
-msgid "Could not fetch article. Run with -vv to see the reason"
+msgid "Could not fetch article."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:863
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:844
+msgid "The debug traceback is available earlier in this log"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:846
+msgid "Run with -vv to see the reason"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:869
msgid "Fetching feeds..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:868
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:874
msgid "Got feeds from index page"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:877
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:883
msgid "Trying to download cover..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:879
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:885
msgid "Generating masthead..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:960
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:965
msgid "Starting download [%d thread(s)]..."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:976
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:981
msgid "Feeds downloaded to %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:985
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:990
msgid "Could not download cover: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:994
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:999
msgid "Downloading cover from %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1040
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1045
msgid "Masthead image downloaded"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1208
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1213
msgid "Untitled Article"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1279
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1284
msgid "Article downloaded: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1290
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1295
msgid "Article download failed: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1307
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1312
msgid "Fetching feed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1454
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1459
msgid "Failed to log in, check your username and password for the calibre Periodicals service."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1469
+#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:1474
msgid "You do not have permission to download this issue. Either your subscription has expired or you have exceeded the maximum allowed downloads for today."
msgstr ""
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index 8f928cfe87..2e5852df89 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -75,7 +75,7 @@ def eval_(self, formatter, kwargs, mi, locals, *args):
exc_type, exc_value, exc_traceback = sys.exc_info()
info = ': '.join(traceback.format_exception(exc_type, exc_value,
exc_traceback)[-2:]).replace('\n', '')
- return _('Exception ' + info)
+ return _('Exception ') + info
all_builtin_functions = []
class BuiltinFormatterFunction(FormatterFunction):
From a2dd6e7d4ec95f482351904a602ee9346598d910 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 11:27:45 -0700
Subject: [PATCH 17/61] ...
---
src/calibre/gui2/convert/search_and_replace.py | 2 +-
src/calibre/translations/calibre.pot | 7 ++++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py
index 04a337a4fc..ba156c5b2a 100644
--- a/src/calibre/gui2/convert/search_and_replace.py
+++ b/src/calibre/gui2/convert/search_and_replace.py
@@ -12,7 +12,7 @@
class SearchAndReplaceWidget(Widget, Ui_Form):
- TITLE = _(u'Search\u00a0&\nReplace')
+ TITLE = _('Search\n&\nReplace')
HELP = _('Modify the document text and structure using user defined patterns.')
COMMIT_NAME = 'search_and_replace'
ICON = I('search.png')
diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot
index dae186fa59..0cdf38f874 100644
--- a/src/calibre/translations/calibre.pot
+++ b/src/calibre/translations/calibre.pot
@@ -5,8 +5,8 @@
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.7.40\n"
-"POT-Creation-Date: 2011-01-19 11:02+MST\n"
-"PO-Revision-Date: 2011-01-19 11:02+MST\n"
+"POT-Creation-Date: 2011-01-19 11:27+MST\n"
+"PO-Revision-Date: 2011-01-19 11:27+MST\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@@ -5690,7 +5690,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace.py:15
msgid ""
-"Search\240&\n"
+"Search\n"
+"&\n"
"Replace"
msgstr ""
From dd30aa375ae5468f4961c7c88718db503d1aee5d Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 13:18:51 -0700
Subject: [PATCH 18/61] Fix #8458 (Las Vegas Review Journal: Extra "stuff" in
each article)
---
resources/recipes/las_vegas_review.recipe | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/resources/recipes/las_vegas_review.recipe b/resources/recipes/las_vegas_review.recipe
index 9292c105a4..ea51c2cf78 100644
--- a/resources/recipes/las_vegas_review.recipe
+++ b/resources/recipes/las_vegas_review.recipe
@@ -3,12 +3,17 @@
class AdvancedUserRecipe1274742400(BasicNewsRecipe):
title = u'Las Vegas Review Journal'
- __author__ = 'Joel'
+ __author__ = 'Kovid Goyal'
language = 'en'
oldest_article = 7
max_articles_per_feed = 100
+ keep_only_tags = [dict(id='content-main')]
+ remove_tags = [dict(id=['right-col-content', 'trending-topics']),
+ {'class':['ppy-outer']}
+ ]
+ no_stylesheets = True
feeds = [
(u'News', u'http://www.lvrj.com/news.rss'),
From 33f524d985a2358bca7b9a5d0bb42d4978dec9cc Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 16:59:20 -0700
Subject: [PATCH 19/61] ...
---
src/calibre/gui2/dialogs/metadata_single.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 4ca2072317..a2ced18e0f 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -724,7 +724,7 @@ def initalize_authors(self):
au = _('Unknown')
au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')])
self.authors.setEditText(au)
-
+
self.authors.set_separator('&')
self.authors.set_space_before_sep(True)
self.authors.update_items_cache(self.db.all_author_names())
From add04ff930f0725bc4af6dfa4eacc0c01ebb98f1 Mon Sep 17 00:00:00 2001
From: GRiker
Date: Wed, 19 Jan 2011 17:01:34 -0700
Subject: [PATCH 20/61] GwR revisions to catalog, making 'By Authors' optional
---
src/calibre/gui2/catalog/catalog_epub_mobi.ui | 6 +-
src/calibre/library/catalog.py | 122 +++++++++++++-----
2 files changed, 92 insertions(+), 36 deletions(-)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
index 6e057a6481..5136f86fae 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui
@@ -35,7 +35,7 @@
- Sections to include in catalog. All catalogs include 'Books by Author'.
+ Sections to include in catalog.
Included sections
@@ -79,13 +79,13 @@
-
- false
+ true
Books by Author
- true
+ false
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index ae600a29f9..6b607b7c1f 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -29,7 +29,6 @@
'series_index', 'series', 'size', 'tags', 'timestamp', 'title',
'uuid']
-
#Allowed fields for template
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
@@ -605,43 +604,42 @@ class EPUB_MOBI(CatalogPlugin):
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-authors',
- default=True,
+ default=False,
dest='generate_authors',
action = 'store_true',
- help=_("Include 'Authors' section in catalog."
- "This switch is ignored - Books By Author section is always generated."
+ help=_("Include 'Authors' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-descriptions',
- default=True,
+ default=False,
dest='generate_descriptions',
action = 'store_true',
- help=_("Include book descriptions in catalog.\n"
+ help=_("Include 'Descriptions' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-genres',
- default=True,
+ default=False,
dest='generate_genres',
action = 'store_true',
help=_("Include 'Genres' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-titles',
- default=True,
+ default=False,
dest='generate_titles',
action = 'store_true',
help=_("Include 'Titles' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-series',
- default=True,
+ default=False,
dest='generate_series',
action = 'store_true',
help=_("Include 'Series' section in catalog.\n"
"Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--generate-recently-added',
- default=True,
+ default=False,
dest='generate_recently_added',
action = 'store_true',
help=_("Include 'Recently Added' section in catalog.\n"
@@ -976,7 +974,7 @@ def __init__(self, db, opts, plugin,
self.__thumbWidth = 0
self.__thumbHeight = 0
self.__title = opts.catalog_title
- self.__totalSteps = 8.0
+ self.__totalSteps = 6.0
self.__useSeriesPrefixInTitlesSection = False
self.__verbose = opts.verbose
@@ -1014,17 +1012,21 @@ def __init__(self, db, opts, plugin,
(self.__archive_path, float(cached_thumb_width)))
# Tweak build steps based on optional sections: 1 call for HTML, 1 for NCX
+ incremental_jobs = 0
+ if self.opts.generate_authors:
+ incremental_jobs += 2
if self.opts.generate_titles:
- self.__totalSteps += 2
+ incremental_jobs += 2
if self.opts.generate_recently_added:
- self.__totalSteps += 2
+ incremental_jobs += 2
if self.generateRecentlyRead:
- self.__totalSteps += 2
+ incremental_jobs += 2
if self.opts.generate_series:
- self.__totalSteps += 2
+ incremental_jobs += 2
if self.opts.generate_descriptions:
# +1 thumbs
- self.__totalSteps += 3
+ incremental_jobs += 3
+ self.__totalSteps += incremental_jobs
# Load section list templates
templates = []
@@ -1358,13 +1360,21 @@ def buildSources(self):
if self.opts.generate_descriptions:
self.generateThumbnails()
self.generateHTMLDescriptions()
- self.generateHTMLByAuthor()
+ if self.opts.generate_authors:
+ self.generateHTMLByAuthor()
if self.opts.generate_titles:
self.generateHTMLByTitle()
if self.opts.generate_series:
self.generateHTMLBySeries()
if self.opts.generate_genres:
self.generateHTMLByTags()
+ # If this is the only Section, and there are no genres, bail
+ if self.opts.section_list == ['Genres'] and not self.genres:
+ error_msg = _("No Genres found to catalog.\nCheck 'Excluded genres'\nin E-book options.\n")
+ self.opts.log.error(error_msg)
+ self.error.append(_('No books available to catalog'))
+ self.error.append(error_msg)
+ return False
if self.opts.generate_recently_added:
self.generateHTMLByDateAdded()
if self.generateRecentlyRead:
@@ -1372,7 +1382,8 @@ def buildSources(self):
self.generateOPF()
self.generateNCXHeader()
- self.generateNCXByAuthor("Authors")
+ if self.opts.generate_authors:
+ self.generateNCXByAuthor("Authors")
if self.opts.generate_titles:
self.generateNCXByTitle("Titles")
if self.opts.generate_series:
@@ -1879,7 +1890,8 @@ def generateHTMLByTitle(self):
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
aTag.insert(0, NavigableString(book['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@@ -2149,7 +2161,8 @@ def add_books_to_HTML_by_month(this_months_list, dtc):
pAuthorTag = Tag(soup, "p")
pAuthorTag['class'] = "author_index"
aTag = Tag(soup, "a")
- aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
+ if self.opts.generate_authors:
+ aTag['name'] = "%s" % self.generateAuthorAnchor(current_author)
aTag.insert(0,NavigableString(current_author))
pAuthorTag.insert(0,aTag)
divTag.insert(dtc,pAuthorTag)
@@ -2276,7 +2289,8 @@ def add_books_to_HTML_by_date_range(date_range_list, date_range, dtc):
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@@ -2425,7 +2439,8 @@ def add_books_to_HTML_by_day(todays_list, dtc):
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@@ -2473,7 +2488,8 @@ def add_books_to_HTML_by_date_range(date_range_list, date_range, dtc):
# Link to author
emTag = Tag(soup, "em")
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author']))
aTag.insert(0, NavigableString(new_entry['author']))
emTag.insert(0,aTag)
pBookTag.insert(ptc, emTag)
@@ -2692,7 +2708,8 @@ def generateHTMLBySeries(self):
# Link to author
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
self.generateAuthorAnchor(escape(' & '.join(book['authors']))))
aTag.insert(0, NavigableString(' & '.join(book['authors'])))
pBookTag.insert(ptc, aTag)
@@ -3074,10 +3091,34 @@ def generateNCXHeader(self):
textTag.insert(0, NavigableString(self.title))
navLabelTag.insert(0, textTag)
navPointTag.insert(0, navLabelTag)
- contentTag = Tag(soup, 'content')
- #contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id'])
- contentTag['src'] = "content/ByAlphaAuthor.html"
- navPointTag.insert(1, contentTag)
+
+ if self.opts.generate_authors:
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/ByAlphaAuthor.html"
+ navPointTag.insert(1, contentTag)
+ elif self.opts.generate_titles:
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/ByAlphaTitle.html"
+ navPointTag.insert(1, contentTag)
+ elif self.opts.generate_series:
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/BySeries.html"
+ navPointTag.insert(1, contentTag)
+ elif self.opts.generate_genres:
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/ByGenres.html"
+ navPointTag.insert(1, contentTag)
+ elif self.opts.generate_recently_added:
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/ByDateAdded.html"
+ navPointTag.insert(1, contentTag)
+ else:
+ sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
+ else self.booksByTitle
+ contentTag = Tag(soup, 'content')
+ contentTag['src'] = "content/book_%d.html" % int(sort_descriptions_by[0]['id'])
+ navPointTag.insert(1, contentTag)
+
cmiTag = Tag(soup, '%s' % 'calibre:meta-img')
cmiTag['name'] = "mastheadImage"
cmiTag['src'] = "images/mastheadImage.gif"
@@ -4140,7 +4181,8 @@ def generateHTMLByGenre(self, genre, section_head, books, outfile):
pAuthorTag = Tag(soup, "p")
pAuthorTag['class'] = "author_index"
aTag = Tag(soup, "a")
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author']))
aTag.insert(0, book['author'])
pAuthorTag.insert(0,aTag)
divTag.insert(dtc,pAuthorTag)
@@ -4371,7 +4413,8 @@ def generate_html():
# Insert the author link (always)
aTag = body.find('a', attrs={'class':'author'})
- aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
+ if self.opts.generate_authors:
+ aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor",
self.generateAuthorAnchor(book['author']))
if publisher == ' ':
@@ -4860,6 +4903,8 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
opts.basename = "Catalog"
opts.cli_environment = not hasattr(opts,'sync')
+
+ # Hard-wired to always sort descriptions by author, with series after non-series
opts.sort_descriptions_by_author = True
build_log = []
@@ -4898,14 +4943,13 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
if opts_dict['ids']:
build_log.append(" book count: %d" % len(opts_dict['ids']))
- '''
sections_list = []
if opts.generate_authors:
sections_list.append('Authors')
- '''
- sections_list = ['Authors']
if opts.generate_titles:
sections_list.append('Titles')
+ if opts.generate_series:
+ sections_list.append('Series')
if opts.generate_genres:
sections_list.append('Genres')
if opts.generate_recently_added:
@@ -4913,7 +4957,18 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
if opts.generate_descriptions:
sections_list.append('Descriptions')
+ if not sections_list:
+ opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
+ opts.log.warn('When invoking from the CLI, add one or more of the Section switches:\n'
+ ' --generate-authors\n'
+ ' --generate-titles\n'
+ ' --generate-series\n'
+ ' --generate-genres\n'
+ ' --generate-recently-added\n'
+ ' --generate-descriptions')
+ return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
build_log.append(u" Sections: %s" % ', '.join(sections_list))
+ opts.section_list = sections_list
# Limit thumb_width to 1.0" - 2.0"
try:
@@ -4948,6 +5003,7 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
# Launch the Catalog builder
catalog = self.CatalogBuilder(db, opts, self, report_progress=notification)
+
if opts.verbose:
log.info(" Begin catalog source generation")
catalog.createDirectoryStructure()
From 6d8f1fc1854b7a68f0cae03e72e06426f97feb97 Mon Sep 17 00:00:00 2001
From: GRiker
Date: Wed, 19 Jan 2011 17:41:09 -0700
Subject: [PATCH 21/61] GwR revisions to catalog generator
---
src/calibre/library/catalog.py | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 6b607b7c1f..b52e3785bd 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -1519,7 +1519,6 @@ def fetchBooksByTitle(self):
for tag in exclude_tags:
search_terms.append("tag:=%s" % tag)
search_phrase = "not (%s)" % " or ".join(search_terms)
-
# If a list of ids are provided, don't use search_text
if self.opts.ids:
self.opts.search_text = search_phrase
@@ -4958,15 +4957,18 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
sections_list.append('Descriptions')
if not sections_list:
- opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
- opts.log.warn('When invoking from the CLI, add one or more of the Section switches:\n'
- ' --generate-authors\n'
- ' --generate-titles\n'
- ' --generate-series\n'
- ' --generate-genres\n'
- ' --generate-recently-added\n'
- ' --generate-descriptions')
- return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
+ if opts.cli_environment:
+ opts.log.warn('*** No Section switches specified, enabling all Sections ***')
+ opts.generate_authors = True
+ opts.generate_titles = True
+ opts.generate_series = True
+ opts.generate_genres = True
+ opts.generate_recently_added = True
+ opts.generate_descriptions = True
+ sections_list = ['Authors','Titles','Series','Genres','Recently Added','Descriptions']
+ else:
+ opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
+ return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
build_log.append(u" Sections: %s" % ', '.join(sections_list))
opts.section_list = sections_list
From 9b80ef4b50597d61f07956b7da656e74ae6016e5 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 17:44:16 -0700
Subject: [PATCH 22/61] Gulf News by DM. Fixes #8466 (New recipe for UAE Gulf
News)
---
resources/recipes/gulfnews.recipe | 64 +++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
create mode 100644 resources/recipes/gulfnews.recipe
diff --git a/resources/recipes/gulfnews.recipe b/resources/recipes/gulfnews.recipe
new file mode 100644
index 0000000000..4c40aa3f37
--- /dev/null
+++ b/resources/recipes/gulfnews.recipe
@@ -0,0 +1,64 @@
+__license__ = 'GPL v3'
+__copyright__ = '2011, Darko Miletic '
+'''
+gulfnews.com
+'''
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class GulfNews(BasicNewsRecipe):
+ title = 'Gulf News'
+ __author__ = 'Darko Miletic'
+ description = 'News from United Arab Emirrates, persian gulf and rest of the world'
+ publisher = 'Al Nisr Publishing LLC'
+ category = 'news, politics, UAE, world'
+ oldest_article = 2
+ max_articles_per_feed = 200
+ no_stylesheets = True
+ encoding = 'utf8'
+ use_embedded_content = False
+ language = 'en'
+ remove_empty_feeds = True
+ publication_type = 'newsportal'
+ masthead_url = 'http://gulfnews.com/media/img/gulf_news_logo.jpg'
+ extra_css = """
+ body{font-family: Arial,Helvetica,sans-serif }
+ img{margin-bottom: 0.4em; display:block}
+ h1{font-family: Georgia, 'Times New Roman', Times, serif}
+ ol,ul{list-style: none}
+ .synopsis{font-size: small}
+ .details{font-size: x-small}
+ .image{font-size: xx-small}
+ """
+
+ conversion_options = {
+ 'comment' : description
+ , 'tags' : category
+ , 'publisher' : publisher
+ , 'language' : language
+ }
+
+ remove_tags = [
+ dict(name=['meta','link','object','embed'])
+ ,dict(attrs={'class':['quickLinks','ratings']})
+ ,dict(attrs={'id':'imageSelector'})
+ ]
+ remove_attributes=['lang']
+ keep_only_tags=[
+ dict(name='h1')
+ ,dict(attrs={'class':['synopsis','details','image','article']})
+ ]
+
+
+ feeds = [
+ (u'UAE News' , u'http://gulfnews.com/cmlink/1.446094')
+ ,(u'Business' , u'http://gulfnews.com/cmlink/1.446098')
+ ,(u'Entertainment' , u'http://gulfnews.com/cmlink/1.446095')
+ ,(u'Sport' , u'http://gulfnews.com/cmlink/1.446096')
+ ,(u'Life' , u'http://gulfnews.com/cmlink/1.446097')
+ ]
+
+ def preprocess_html(self, soup):
+ for item in soup.findAll(style=True):
+ del item['style']
+ return soup
From 260484f5152e0892a393be74dff1f2cdee891d36 Mon Sep 17 00:00:00 2001
From: ldolse
Date: Thu, 20 Jan 2011 10:06:28 +0800
Subject: [PATCH 23/61] reduce false positives in dehyphenate
---
src/calibre/ebooks/conversion/preprocess.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py
index f728bec52b..5fceeb7aed 100644
--- a/src/calibre/ebooks/conversion/preprocess.py
+++ b/src/calibre/ebooks/conversion/preprocess.py
@@ -201,7 +201,7 @@ def dehyphenate(self, match):
lookupword = self.removesuffixes.sub('', dehyphenated)
else:
lookupword = dehyphenated
- if len(firsthalf) > 3 and self.prefixes.match(firsthalf) is None:
+ if len(firsthalf) > 4 and self.prefixes.match(firsthalf) is None:
lookupword = self.removeprefix.sub('', lookupword)
if self.verbose > 2:
self.log("lookup word is: "+str(lookupword)+", orig is: " + str(hyphenated))
From 111b4b12a415972bc910b87591f47864b78003f7 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 19:38:26 -0700
Subject: [PATCH 24/61] ...
---
src/calibre/manual/faq.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 37d18ea329..3e382c8f10 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -108,8 +108,8 @@ Follow these steps to find the problem:
* Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time.
* Make sure you are running the latest version of |app|. The latest version can always be downloaded from `the calibre website `_.
- * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is
- * In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled.
+ * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is.
+ * In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker `_.
How does |app| manage collections on my SONY reader?
From 3e5a31683931769dfcf90ceea500d0e5a413f180 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 20:08:39 -0700
Subject: [PATCH 25/61] PD Novel driver: Put books on the SD card into the
eBoks folder
---
src/calibre/devices/misc.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py
index 9f8dbcb379..e549a4a9fd 100644
--- a/src/calibre/devices/misc.py
+++ b/src/calibre/devices/misc.py
@@ -106,7 +106,7 @@ class PDNOVEL(USBMS):
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 130
- EBOOK_DIR_MAIN = 'eBooks'
+ EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'eBooks'
SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
From dd976263248a97e72a9208d2e4e05d13d65eb17b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 20:43:47 -0700
Subject: [PATCH 26/61] Start work on refactored Edit Metadata dialog
---
src/calibre/gui2/metadata/single.py | 535 ++++++++++++++++++++++++++++
src/calibre/gui2/widgets.py | 7 +-
2 files changed, 539 insertions(+), 3 deletions(-)
create mode 100644 src/calibre/gui2/metadata/single.py
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
new file mode 100644
index 0000000000..eff6f97e7d
--- /dev/null
+++ b/src/calibre/gui2/metadata/single.py
@@ -0,0 +1,535 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import textwrap, re
+
+from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
+ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
+ QDoubleSpinBox
+
+from calibre.gui2 import ResizableDialog
+from calibre.utils.icu import sort_key
+from calibre.utils.config import tweaks
+from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
+ EnComboBox
+from calibre.ebooks.metadata import title_sort, authors_to_string, \
+ string_to_authors
+
+'''
+The interface common to all widgets used to set basic metadata
+class BasicMetadataWidget(object):
+
+ LABEL = "label text"
+
+ def initialize(self, db, id_):
+ pass
+
+ def commit(self, db, id_):
+ return True
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return None
+ def fset(self, val):
+ pass
+ return property(fget=fget, fset=fset)
+'''
+
+# Title {{{
+class TitleEdit(EnLineEdit):
+
+ TITLE_ATTR = 'title'
+ COMMIT = True
+ TOOLTIP = _('Change the title of this book')
+ LABEL = _('&Title:')
+
+ def __init__(self, parent):
+ self.dialog = parent
+ EnLineEdit.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+
+ def get_default(self):
+ return _('Unknown')
+
+ def initialize(self, db, id_):
+ title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
+ self.current_val = title
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ title = self.current_val
+ if self.COMMIT:
+ getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
+ else:
+ getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
+ commit=False)
+ return True
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ title = unicode(self.text()).strip()
+ if not title:
+ title = self.get_default()
+ return title
+
+ def fset(self, val):
+ if hasattr(val, 'strip'):
+ val = val.strip()
+ if not val:
+ val = self.get_default()
+ self.setText(val)
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+class TitleSortEdit(TitleEdit):
+
+ TITLE_ATTR = 'title_sort'
+ COMMIT = False
+ TOOLTIP = _('Specify how this book should be sorted when by title.'
+ ' For example, The Exorcist might be sorted as Exorcist, The.')
+ LABEL = _('Title &sort:')
+
+ def __init__(self, parent, title_edit, autogen_button):
+ TitleEdit.__init__(self, parent)
+ self.title_edit = title_edit
+
+ base = self.TOOLTIP
+ ok_tooltip = '' + textwrap.fill(base+'
'+
+ _(' The green color indicates that the current '
+ 'title sort matches the current title'))
+ bad_tooltip = '
'+textwrap.fill(base + '
'+
+ _(' The red color warns that the current '
+ 'title sort does not match the current title. '
+ 'No action is required if this is what you want.'))
+ self.tooltips = (ok_tooltip, bad_tooltip)
+
+ self.title_edit.textChanged.connect(self.update_state)
+ self.textChanged.connect(self.update_state)
+
+ autogen_button.clicked.connect(self.auto_generate)
+ self.update_state()
+
+ def update_state(self, *args):
+ ts = title_sort(self.title_edit.current_val)
+ normal = ts == self.current_val
+ if normal:
+ col = 'rgb(0, 255, 0, 20%)'
+ else:
+ col = 'rgb(255, 0, 0, 20%)'
+ self.setStyleSheet('QLineEdit { color: black; '
+ 'background-color: %s; }'%col)
+ tt = self.tooltips[0 if normal else 1]
+ self.setToolTip(tt)
+ self.setWhatsThis(tt)
+
+ def auto_generate(self, *args):
+ self.current_val = title_sort(self.title_edit.current_val)
+
+# }}}
+
+# Authors {{{
+class AuthorsEdit(CompleteComboBox):
+
+ TOOLTIP = ''
+ LABEL = _('&Author(s):')
+
+ def __init__(self, parent):
+ self.dialog = parent
+ CompleteComboBox.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setEditable(True)
+ self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
+
+ def get_default(self):
+ return _('Unknown')
+
+ def initialize(self, db, id_):
+ all_authors = db.all_authors()
+ all_authors.sort(key=lambda x : sort_key(x[1]))
+ for i in all_authors:
+ id, name = i
+ name = [name.strip().replace('|', ',') for n in name.split(',')]
+ self.addItem(authors_to_string(name))
+
+ self.set_separator('&')
+ self.set_space_before_sep(True)
+ self.update_items_cache(db.all_author_names())
+
+ au = db.authors(id_, index_is_id=True)
+ if not au:
+ au = _('Unknown')
+ self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ authors = self.current_val
+ db.set_authors(id_, authors, notify=False)
+ return True
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ au = unicode(self.text()).strip()
+ if not au:
+ au = self.get_default()
+ return string_to_authors(au)
+
+ def fset(self, val):
+ if not val:
+ val = [self.get_default()]
+ self.setEditText(' & '.join([x.strip() for x in val]))
+ self.lineEdit().setCursorPosition(0)
+
+
+ return property(fget=fget, fset=fset)
+
+class AuthorSortEdit(EnLineEdit):
+
+ TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
+ 'For example Charles Dickens should be sorted as Dickens, '
+ 'Charles.\nIf the box is colored green, then text matches '
+ 'the individual author\'s sort strings. If it is colored '
+ 'red, then the authors and this text do not match.')
+ LABEL = _('Author s&ort:')
+
+ def __init__(self, parent, authors_edit, autogen_button, db):
+ EnLineEdit.__init__(self, parent)
+ self.authors_edit = authors_edit
+ self.db = db
+
+ base = self.TOOLTIP
+ ok_tooltip = '
' + textwrap.fill(base+'
'+
+ _(' The green color indicates that the current '
+ 'author sort matches the current author'))
+ bad_tooltip = '
'+textwrap.fill(base + '
'+
+ _(' The red color indicates that the current '
+ 'author sort does not match the current author. '
+ 'No action is required if this is what you want.'))
+ self.tooltips = (ok_tooltip, bad_tooltip)
+
+ self.authors_edit.editTextChanged.connect(self.update_state)
+ self.textChanged.connect(self.update_state)
+
+ autogen_button.clicked.connect(self.auto_generate)
+ self.update_state()
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.text()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def update_state(self, *args):
+ au = unicode(self.authors_edit.text())
+ au = re.sub(r'\s+et al\.$', '', au)
+ au = self.db.author_sort_from_authors(string_to_authors(au))
+
+ normal = au == self.current_val
+ if normal:
+ col = 'rgb(0, 255, 0, 20%)'
+ else:
+ col = 'rgb(255, 0, 0, 20%)'
+ self.setStyleSheet('QLineEdit { color: black; '
+ 'background-color: %s; }'%col)
+ tt = self.tooltips[0 if normal else 1]
+ self.setToolTip(tt)
+ self.setWhatsThis(tt)
+
+ def auto_generate(self, *args):
+ au = unicode(self.authors_edit.text())
+ au = re.sub(r'\s+et al\.$', '', au)
+ authors = string_to_authors(au)
+ self.current_val = self.db.author_sort_from_authors(authors)
+
+ def initialize(self, db, id_):
+ self.current_val = db.author_sort(id_, index_is_id=True)
+
+ def commit(self, db, id_):
+ aus = self.current_val
+ db.set_author_sort(id_, aus, notify=False, commit=False)
+ return True
+
+# }}}
+
+# Series {{{
+class SeriesEdit(EnComboBox):
+
+ TOOLTIP = _('List of known series. You can add new series.')
+ LABEL = _('&Series:')
+
+ def __init__(self, parent):
+ EnComboBox.__init__(self, parent)
+ self.dialog = parent
+ self.setSizeAdjustPolicy(
+ self.AdjustToMinimumContentsLengthWithIcon)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setEditable(True)
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.currentText()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setEditText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ all_series = db.all_series()
+ all_series.sort(key=lambda x : sort_key(x[1]))
+ series_id = db.series_id(id_, index_is_id=True)
+ idx, c = None, 0
+ for i in all_series:
+ id, name = i
+ if id == series_id:
+ idx = c
+ self.addItem(name)
+ c += 1
+
+ self.lineEdit().setText('')
+ if idx is not None:
+ self.setCurrentIndex(idx)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ series = self.current_val
+ db.set_series(id_, series, notify=False, commit=True)
+ return True
+
+class SeriesIndexEdit(QDoubleSpinBox):
+
+ TOOLTIP = ''
+ LABEL = _('&Number:')
+
+ def __init__(self, parent, series_edit):
+ QDoubleSpinBox.__init__(self, parent)
+ self.dialog = parent
+ self.db = self.original_series_name = None
+ self.setMaximum(1000000)
+ self.series_edit = series_edit
+ series_edit.currentIndexChanged.connect(self.enable)
+ series_edit.editTextChanged.connect(self.enable)
+ series_edit.lineEdit().editingFinished.connect(self.increment)
+ self.enable()
+
+ def enable(self, *args):
+ self.setEnabled(bool(self.series_edit.current_val))
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return self.value()
+
+ def fset(self, val):
+ if val is None:
+ val = 1.0
+ val = float(val)
+ self.setValue(val)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.db = db
+ if self.series_edit.current_val:
+ val = db.series_index(id_, index_is_id=True)
+ else:
+ val = 1.0
+ self.current_val = val
+ self.original_val = self.current_val
+ self.original_series_name = self.series_edit.original_val
+
+ def commit(self, db, id_):
+ db.set_series_index(id_, self.current_val, notify=False, commit=False)
+ return True
+
+ def increment(self):
+ if self.db is not None:
+ try:
+ series = self.series_edit.current_val
+ if series and series != self.original_series_name:
+ ns = 1.0
+ if tweaks['series_index_auto_increment'] != 'const':
+ ns = self.db.get_next_series_num_for(series)
+ self.current_val = ns
+ self.original_series_name = series
+ except:
+ import traceback
+ traceback.print_exc()
+
+
+# }}}
+
+class BuddyLabel(QLabel):
+
+ def __init__(self, buddy):
+ QLabel.__init__(self, buddy.LABEL)
+ self.setBuddy(buddy)
+ self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+
+class MetadataSingleDialog(ResizableDialog):
+
+ def __init__(self, db, parent=None):
+ self.db = db
+ ResizableDialog.__init__(self, parent)
+
+ def setupUi(self, *args): # {{{
+ self.resize(990, 650)
+
+ self.button_box = QDialogButtonBox(
+ QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Horizontal,
+ self)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+
+ self.scroll_area = QScrollArea(self)
+ self.scroll_area.setFrameShape(QScrollArea.NoFrame)
+ self.scroll_area.setWidgetResizable(True)
+ self.central_widget = QTabWidget(self)
+ self.scroll_area.setWidget(self.central_widget)
+
+ self.l = QVBoxLayout(self)
+ self.setLayout(self.l)
+ self.l.setMargin(0)
+ self.l.addWidget(self.scroll_area)
+ self.l.addWidget(self.button_box)
+
+ self.setWindowIcon(QIcon(I('edit_input.png')))
+ self.setWindowTitle(_('Edit Meta Information'))
+
+ self.create_basic_metadata_widgets()
+
+ self.do_layout()
+ # }}}
+
+ def create_basic_metadata_widgets(self):
+ self.basic_metadata_widgets = []
+ # Title
+ self.title = TitleEdit(self)
+ self.deduce_title_sort_button = QToolButton(self)
+ self.deduce_title_sort_button.setToolTip(
+ _('Automatically create the title sort entry based on the current '
+ 'title entry.\nUsing this button to create title sort will '
+ 'change title sort from red to green.'))
+ self.deduce_title_sort_button.setWhatsThis(
+ self.deduce_title_sort_button.toolTip())
+ self.title_sort = TitleSortEdit(self, self.title,
+ self.deduce_title_sort_button)
+ self.basic_metadata_widgets.extend([self.title, self.title_sort])
+
+ # Authors
+ self.authors = AuthorsEdit(self)
+ self.deduce_author_sort_button = QToolButton(self)
+ self.deduce_author_sort_button.setToolTip(_(
+ 'Automatically create the author sort entry based on the current'
+ ' author entry.\n'
+ 'Using this button to create author sort will change author sort from'
+ ' red to green.'))
+ self.author_sort = AuthorSortEdit(self, self.authors,
+ self.deduce_author_sort_button, db)
+ self.basic_metadata_widgets.extend([self.authors, self.author_sort])
+
+ self.swap_title_author_button = QToolButton(self)
+ self.swap_title_author_button.setIcon(QIcon(I('swap.png')))
+ self.swap_title_author_button.setToolTip(_(
+ 'Swap the author and title'))
+ self.swap_title_author_button.clicked.connect(self.swap_title_author)
+
+ self.series = SeriesEdit(self)
+ self.remove_unused_series_button = QToolButton(self)
+ self.remove_unused_series_button.setToolTip(
+ _('Remove unused series (Series that have no books)') )
+ self.remove_unused_series_button.clicked.connect(self.remove_unused_series)
+ self.series_index = SeriesIndexEdit(self, self.series)
+ self.basic_metadata_widgets.extend([self.series, self.series_index])
+
+ def do_layout(self):
+ self.central_widget.clear()
+ self.tabs = []
+ self.labels = []
+ self.tabs.append(QWidget(self))
+ self.central_widget.addTab(self.tabs[0], _("&Basic metadata"))
+ self.tabs[0].l = l = QVBoxLayout()
+ self.tabs[0].tl = tl = QGridLayout()
+ self.tabs[0].setLayout(l)
+ l.addLayout(tl)
+
+ def create_row(row, one, two, three, col=1, icon='forward.png'):
+ ql = BuddyLabel(one)
+ tl.addWidget(ql, row, col+0, 1, 1)
+ self.labels.append(ql)
+ tl.addWidget(one, row, col+1, 1, 1)
+ if two is not None:
+ tl.addWidget(two, row, col+2, 1, 1)
+ two.setIcon(QIcon(I(icon)))
+ ql = BuddyLabel(three)
+ tl.addWidget(ql, row, col+3, 1, 1)
+ self.labels.append(ql)
+ tl.addWidget(three, row, col+4, 1, 1)
+
+ tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
+
+ create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
+ create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
+ create_row(2, self.series, self.remove_unused_series_button,
+ self.series_index, icon='trash.png')
+
+
+ def __call__(self, id_, has_next=False, has_previous=False):
+ self.book_id = id_
+ for widget in self.basic_metadata_widgets:
+ widget.initialize(self.db, id_)
+
+ def swap_title_author(self, *args):
+ title = self.title.current_val
+ self.title.current_val = authors_to_string(self.authors.current_val)
+ self.authors.current_val = string_to_authors(title)
+ self.title_sort.auto_generate()
+ self.author_sort.auto_generate()
+
+ def remove_unused_series(self, *args):
+ self.db.remove_unused_series()
+ idx = self.series.current_val
+ self.series.clear()
+ self.series.initialize(self.db, self.book_id)
+ if idx:
+ for i in range(self.series.count()):
+ if unicode(self.series.itemText(i)) == idx:
+ self.series.setCurrentIndex(i)
+ break
+
+
+if __name__ == '__main__':
+ from PyQt4.Qt import QApplication
+ app = QApplication([])
+ from calibre.library import db
+ db = db()
+ d = MetadataSingleDialog(db)
+ d(db.data[0][0])
+ d.exec_()
+
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index dd1121c725..fc21d9a3b3 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -123,6 +123,8 @@ def commit(self):
class FormatList(QListWidget):
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
+ formats_dropped = pyqtSignal(object, object)
+ delete_format = pyqtSignal()
@classmethod
def paths_from_event(cls, event):
@@ -146,15 +148,14 @@ def dragEnterEvent(self, event):
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
- self.emit(SIGNAL('formats_dropped(PyQt_PyObject,PyQt_PyObject)'),
- event, paths)
+ self.formats_dropped.emit(event, paths)
def dragMoveEvent(self, event):
event.acceptProposedAction()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
- self.emit(SIGNAL('delete_format()'))
+ self.delete_format.emit()
else:
return QListWidget.keyPressEvent(self, event)
From 3a063ee644de603d3e3c8c327c58b1f95abfc416 Mon Sep 17 00:00:00 2001
From: ldolse
Date: Thu, 20 Jan 2011 12:10:34 +0800
Subject: [PATCH 27/61] updated text input heuristics option to preserve text
indents, fixed false positive case in dehyphenate
---
src/calibre/ebooks/txt/input.py | 51 +++++++++++++++++----------------
1 file changed, 27 insertions(+), 24 deletions(-)
diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py
index 5b99b19e74..6ec1edb65c 100644
--- a/src/calibre/ebooks/txt/input.py
+++ b/src/calibre/ebooks/txt/input.py
@@ -71,21 +71,41 @@ def convert(self, stream, options, file_ext, log,
txt = txt.decode(ienc, 'replace')
txt = _ent_pat.sub(xml_entity_to_unicode, txt)
+
+ # Normalize line endings
+ txt = normalize_line_endings(txt)
+
+ if options.formatting_type == 'auto':
+ options.formatting_type = detect_formatting_type(txt)
+
+ if options.formatting_type == 'heuristic':
+ setattr(options, 'enable_heuristics', True)
+ setattr(options, 'markup_chapter_headings', True)
+ setattr(options, 'italicize_common_cases', True)
+ setattr(options, 'fix_indents', True)
+ setattr(options, 'preserve_spaces', True)
+ setattr(options, 'delete_blank_paragraphs', True)
+ setattr(options, 'format_scene_breaks', True)
+ setattr(options, 'dehyphenate', True)
+
+ # Determine the paragraph type of the document.
+ if options.paragraph_type == 'auto':
+ options.paragraph_type = detect_paragraph_type(txt)
+ if options.paragraph_type == 'unknown':
+ log.debug('Could not reliably determine paragraph type using block')
+ options.paragraph_type = 'block'
+ else:
+ log.debug('Auto detected paragraph type as %s' % options.paragraph_type)
+
# Preserve spaces will replace multiple spaces to a space
# followed by the entity.
if options.preserve_spaces:
txt = preserve_spaces(txt)
- # Normalize line endings
- txt = normalize_line_endings(txt)
-
# Get length for hyphen removal and punctuation unwrap
docanalysis = DocAnalysis('txt', txt)
length = docanalysis.line_length(.5)
- if options.formatting_type == 'auto':
- options.formatting_type = detect_formatting_type(txt)
-
if options.formatting_type == 'markdown':
log.debug('Running text though markdown conversion...')
try:
@@ -96,16 +116,8 @@ def convert(self, stream, options, file_ext, log,
elif options.formatting_type == 'textile':
log.debug('Running text though textile conversion...')
html = convert_textile(txt)
- else:
- # Determine the paragraph type of the document.
- if options.paragraph_type == 'auto':
- options.paragraph_type = detect_paragraph_type(txt)
- if options.paragraph_type == 'unknown':
- log.debug('Could not reliably determine paragraph type using block')
- options.paragraph_type = 'block'
- else:
- log.debug('Auto detected paragraph type as %s' % options.paragraph_type)
+ else:
# Dehyphenate
dehyphenator = Dehyphenator(options.verbose, log=self.log)
txt = dehyphenator(txt,'txt', length)
@@ -129,15 +141,6 @@ def convert(self, stream, options, file_ext, log,
flow_size = getattr(options, 'flow_size', 0)
html = convert_basic(txt, epub_split_size_kb=flow_size)
- if options.formatting_type == 'heuristic':
- setattr(options, 'enable_heuristics', True)
- setattr(options, 'markup_chapter_headings', True)
- setattr(options, 'italicize_common_cases', True)
- setattr(options, 'fix_indents', True)
- setattr(options, 'delete_blank_paragraphs', True)
- setattr(options, 'format_scene_breaks', True)
- setattr(options, 'dehyphenate', True)
-
from calibre.customize.ui import plugin_for_input_format
html_input = plugin_for_input_format('html')
for opt in html_input.options:
From 2f5fa30b8680ef69608552aa38190beb71cfaa35 Mon Sep 17 00:00:00 2001
From: ldolse
Date: Thu, 20 Jan 2011 12:17:25 +0800
Subject: [PATCH 28/61] ...
---
src/calibre/ebooks/conversion/cli.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py
index a95bb23126..33ae61f16a 100644
--- a/src/calibre/ebooks/conversion/cli.py
+++ b/src/calibre/ebooks/conversion/cli.py
@@ -75,7 +75,6 @@ def option_recommendation_to_cli_option(add_option, rec):
opt = rec.option
switches = ['-'+opt.short_switch] if opt.short_switch else []
switches.append('--'+opt.long_switch)
- flip_switches = ['italicize_common_cases', 'markup_chapter_headings', 'unwrap_lines', 'dehyphenate', 'fix_indents']
attrs = dict(dest=opt.name, help=opt.help,
choices=opt.choices, default=rec.recommended_value)
if isinstance(rec.recommended_value, type(True)):
From f6404d19d856b35085a3f337eb303872bd5d638c Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 19 Jan 2011 22:29:11 -0700
Subject: [PATCH 29/61] More work on the new metadata dialog
---
src/calibre/gui2/metadata/single.py | 194 +++++++++++++++++++++++++++-
1 file changed, 187 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index eff6f97e7d..be4170b43f 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -5,19 +5,26 @@
__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import textwrap, re
+import textwrap, re, os
from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
- QDoubleSpinBox
+ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal
-from calibre.gui2 import ResizableDialog
+from calibre.gui2 import ResizableDialog, file_icon_provider, \
+ choose_files, error_dialog
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox
+ EnComboBox, FormatList
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors
+from calibre.utils.date import local_tz
+from calibre import strftime
+from calibre.ebooks import BOOK_EXTENSIONS
+from calibre.customize.ui import run_plugins_on_import
+from calibre.utils.date import utcfromtimestamp
+
'''
The interface common to all widgets used to set basic metadata
@@ -385,15 +392,181 @@ def increment(self):
# }}}
-class BuddyLabel(QLabel):
+class BuddyLabel(QLabel): # {{{
def __init__(self, buddy):
QLabel.__init__(self, buddy.LABEL)
self.setBuddy(buddy)
self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+# }}}
+
+class Format(QListWidgetItem): # {{{
+
+ def __init__(self, parent, ext, size, path=None, timestamp=None):
+ self.path = path
+ self.ext = ext
+ self.size = float(size)/(1024*1024)
+ text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
+ QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
+ text, parent, QListWidgetItem.UserType)
+ if timestamp is not None:
+ ts = timestamp.astimezone(local_tz)
+ t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
+ text = _('Last modified: %s')%t
+ self.setToolTip(text)
+ self.setStatusTip(text)
+
+# }}}
+
+class FormatsManager(QWidget): # {{{
+
+ def __init__(self, parent):
+ QWidget.__init__(self, parent)
+ self.dialog = parent
+ self.changed = False
+
+ self.l = l = QGridLayout()
+ self.setLayout(l)
+ self.cover_from_format_button = QToolButton(self)
+ self.cover_from_format_button.setToolTip(
+ _('Set the cover for the book from the selected format'))
+ self.cover_from_format_button.setIcon(QIcon(I('book.png')))
+ self.cover_from_format_button.setIconSize(QSize(32, 32))
+
+ self.metadata_from_format_button = QToolButton(self)
+ self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
+ self.metadata_from_format_button.setIconSize(QSize(32, 32))
+ # TODO: Implement the *_from_format buttons
+
+ self.add_format_button = QToolButton(self)
+ self.add_format_button.setIcon(QIcon(I('add_book.png')))
+ self.add_format_button.setIconSize(QSize(32, 32))
+ self.add_format_button.clicked.connect(self.add_format)
+
+ self.remove_format_button = QToolButton(self)
+ self.remove_format_button.setIcon(QIcon(I('trash.png')))
+ self.remove_format_button.setIconSize(QSize(32, 32))
+ self.remove_format_button.clicked.connect(self.remove_format)
+
+ self.formats = FormatList(self)
+ self.formats.setAcceptDrops(True)
+ self.formats.formats_dropped.connect(self.formats_dropped)
+ self.formats.delete_format.connect(self.remove_format)
+ self.formats.itemDoubleClicked.connect(self.show_format)
+ self.formats.setDragDropMode(self.formats.DropOnly)
+ self.formats.setIconSize(QSize(32, 32))
+ self.formats.setMaximumWidth(200)
+
+ l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
+ l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
+ l.addWidget(self.add_format_button, 0, 2, 1, 1)
+ l.addWidget(self.remove_format_button, 2, 2, 1, 1)
+ l.addWidget(self.formats, 0, 1, 3, 1)
+
+
+
+ def initialize(self, db, id_):
+ self.changed = False
+ exts = db.formats(id_, index_is_id=True)
+ if exts:
+ exts = exts.split(',')
+ for ext in exts:
+ if not ext:
+ ext = ''
+ size = db.sizeof_format(id_, ext, index_is_id=True)
+ timestamp = db.format_last_modified(id_, ext)
+ if size is None:
+ continue
+ Format(self.formats, ext, size, timestamp=timestamp)
+
+ def commit(self, db, id_):
+ if not self.changed:
+ return True
+ old_extensions, new_extensions, paths = set(), set(), {}
+ for row in range(self.formats.count()):
+ fmt = self.formats.item(row)
+ ext, path = fmt.ext.lower(), fmt.path
+ if 'unknown' in ext.lower():
+ ext = None
+ if path:
+ new_extensions.add(ext)
+ paths[ext] = path
+ else:
+ old_extensions.add(ext)
+ for ext in new_extensions:
+ db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
+ index_is_id=True)
+ db_extensions = set([f.lower() for f in db.formats(id_,
+ index_is_id=True).split(',')])
+ extensions = new_extensions.union(old_extensions)
+ for ext in db_extensions:
+ if ext not in extensions:
+ db.remove_format(id_, ext, notify=False, index_is_id=True)
+
+ self.changed = False
+ return True
+
+ def add_format(self, *args):
+ files = choose_files(self, 'add formats dialog',
+ _("Choose formats for ") +
+ self.dialog.title.current_val,
+ [(_('Books'), BOOK_EXTENSIONS)])
+ self._add_formats(files)
+
+ def _add_formats(self, paths):
+ added = False
+ if not paths:
+ return added
+ bad_perms = []
+ for _file in paths:
+ _file = os.path.abspath(_file)
+ if not os.access(_file, os.R_OK):
+ bad_perms.append(_file)
+ continue
+
+ nfile = run_plugins_on_import(_file)
+ if nfile is not None:
+ _file = nfile
+ stat = os.stat(_file)
+ size = stat.st_size
+ ext = os.path.splitext(_file)[1].lower().replace('.', '')
+ timestamp = utcfromtimestamp(stat.st_mtime)
+ for row in range(self.formats.count()):
+ fmt = self.formats.item(row)
+ if fmt.ext.lower() == ext:
+ self.formats.takeItem(row)
+ break
+ Format(self.formats, ext, size, path=_file, timestamp=timestamp)
+ self.changed = True
+ added = True
+ if bad_perms:
+ error_dialog(self, _('No permission'),
+ _('You do not have '
+ 'permission to read the following files:'),
+ det_msg='\n'.join(bad_perms), show=True)
+
+ return added
+
+ def formats_dropped(self, event, paths):
+ if self._add_formats(paths):
+ event.accept()
+
+ def remove_format(self, *args):
+ rows = self.formats.selectionModel().selectedRows(0)
+ for row in rows:
+ self.formats.takeItem(row.row())
+ self.changed = True
+
+ def show_format(self, item, *args):
+ fmt = item.ext
+ self.dialog.view_format.emit(fmt)
+
+# }}}
class MetadataSingleDialog(ResizableDialog):
+ view_format = pyqtSignal(object)
+
def __init__(self, db, parent=None):
self.db = db
ResizableDialog.__init__(self, parent)
@@ -427,7 +600,7 @@ def setupUi(self, *args): # {{{
self.do_layout()
# }}}
- def create_basic_metadata_widgets(self):
+ def create_basic_metadata_widgets(self): # {{{
self.basic_metadata_widgets = []
# Title
self.title = TitleEdit(self)
@@ -468,7 +641,12 @@ def create_basic_metadata_widgets(self):
self.series_index = SeriesIndexEdit(self, self.series)
self.basic_metadata_widgets.extend([self.series, self.series_index])
- def do_layout(self):
+ self.formats_manager = FormatsManager(self)
+ self.basic_metadata_widgets.append(self.formats_manager)
+
+ # }}}
+
+ def do_layout(self): # {{{
self.central_widget.clear()
self.tabs = []
self.labels = []
@@ -499,6 +677,8 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
create_row(2, self.series, self.remove_unused_series_button,
self.series_index, icon='trash.png')
+ tl.addWidget(self.formats_manager, 0, 6, 3, 1)
+ # }}}
def __call__(self, id_, has_next=False, has_previous=False):
self.book_id = id_
From 2216de65f07d11a02cbfab0f6c461d7d13427de8 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 00:40:36 -0700
Subject: [PATCH 30/61] ...
---
src/calibre/gui2/metadata/single.py | 74 ++++++++++++++++++++++++++++-
src/calibre/gui2/widgets.py | 6 +--
2 files changed, 75 insertions(+), 5 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index be4170b43f..b9fae51789 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -9,14 +9,15 @@
from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
- QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal
+ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
+ QSplitter
from calibre.gui2 import ResizableDialog, file_icon_provider, \
choose_files, error_dialog
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox, FormatList
+ EnComboBox, FormatList, ImageView
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors
from calibre.utils.date import local_tz
@@ -40,6 +41,7 @@ def commit(self, db, id_):
@dynamic_property
def current_val(self):
+ # Present in most but not all basic metadata widgets
def fget(self):
return None
def fset(self, val):
@@ -563,6 +565,66 @@ def show_format(self, item, *args):
# }}}
+class Cover(ImageView):
+
+ def __init__(self, parent):
+ ImageView.__init__(self, parent)
+ self._cdata = None
+ self.cover_changed.connect(self.set_pixmap_from_data)
+
+ def set_pixmap_from_data(self, data):
+ if not data:
+ self.current_val = None
+ return
+ orig = self.current_val
+ self.current_val = data
+ if self.current_val is None:
+ error_dialog(self, _('Invalid cover'),
+ _('Could not change cover as the image is invalid.'),
+ show=True)
+ self.current_val = orig
+
+ def initialize(self, db, id_):
+ self._cdata = None
+ self.current_val = db.cover(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ @property
+ def changed(self):
+ return self.current_val != self.original_val
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self._cdata
+ def fset(self, cdata):
+ self._cdata = None
+ pm = QPixmap()
+ if cdata:
+ pm.loadFromData(cdata)
+ if pm.isNull():
+ pm = QPixmap(I('default_cover.png'))
+ else:
+ self._cdata = cdata
+ self.setPixmap(pm)
+ tt = _('This book has no cover')
+ if self._cdata:
+ tt = _('Cover size: %dx%d pixels') % \
+ (pm.width(), pm.height())
+ self.setToolTip(tt)
+
+ return property(fget=fget, fset=fset)
+
+ def commit(self, db, id_):
+ if self.changed:
+ if self.current_val:
+ db.set_cover(id_, self.current_val, notify=False, commit=False)
+ else:
+ db.remove_cover(id_, notify=False, commit=False)
+ return True
+
+
+
class MetadataSingleDialog(ResizableDialog):
view_format = pyqtSignal(object)
@@ -644,6 +706,9 @@ def create_basic_metadata_widgets(self): # {{{
self.formats_manager = FormatsManager(self)
self.basic_metadata_widgets.append(self.formats_manager)
+ self.cover = Cover(self)
+ self.basic_metadata_widgets.append(self.cover)
+
# }}}
def do_layout(self): # {{{
@@ -678,9 +743,14 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
self.series_index, icon='trash.png')
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
+
+ self.splitter = QSplitter(Qt.Horizontal, self)
+ self.splitter.addWidget(self.cover)
+ l.addWidget(self.splitter)
# }}}
def __call__(self, id_, has_next=False, has_previous=False):
+ # TODO: Next and previous buttons
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index fc21d9a3b3..9e117822e4 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -163,6 +163,7 @@ def keyPressEvent(self, event):
class ImageView(QWidget):
BORDER_WIDTH = 1
+ cover_changed = pyqtSignal(object)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
@@ -202,8 +203,7 @@ def dropEvent(self, event):
if not pmap.isNull():
self.setPixmap(pmap)
event.accept()
- self.emit(SIGNAL('cover_changed(PyQt_PyObject)'), open(path,
- 'rb').read())
+ self.cover_changed.emit(open(path, 'rb').read())
break
def dragMoveEvent(self, event):
@@ -272,7 +272,7 @@ def paste_from_clipboard(self):
pmap = cb.pixmap(cb.Selection)
if not pmap.isNull():
self.setPixmap(pmap)
- self.emit(SIGNAL('cover_changed(PyQt_PyObject)'),
+ self.cover_changed.emit(
pixmap_to_data(pmap))
# }}}
From 6942bf950c88fe17f61f60cf355103d221e9cfae Mon Sep 17 00:00:00 2001
From: GRiker
Date: Thu, 20 Jan 2011 07:03:19 -0700
Subject: [PATCH 31/61] GwR bug fixes for single-section MOBI output
---
src/calibre/library/catalog.py | 88 ++++++++++++++++++----------------
1 file changed, 48 insertions(+), 40 deletions(-)
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index b52e3785bd..95e738dd58 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -580,7 +580,7 @@ class EPUB_MOBI(CatalogPlugin):
"pipeline to the specified "
"directory. Useful if you are unsure at which stage "
"of the conversion process a bug is occurring.\n"
- "Default: '%default'None\n"
+ "Default: '%default'\n"
"Applies to: ePub, MOBI output formats")),
Option('--exclude-book-marker',
default=':',
@@ -1370,7 +1370,9 @@ def buildSources(self):
self.generateHTMLByTags()
# If this is the only Section, and there are no genres, bail
if self.opts.section_list == ['Genres'] and not self.genres:
- error_msg = _("No Genres found to catalog.\nCheck 'Excluded genres'\nin E-book options.\n")
+ error_msg = _("No enabled genres found to catalog.\n")
+ if not self.opts.cli_environment:
+ error_msg += "Check 'Excluded genres'\nin E-book options.\n"
self.opts.log.error(error_msg)
self.error.append(_('No books available to catalog'))
self.error.append(error_msg)
@@ -2792,14 +2794,16 @@ def generateHTMLByTags(self):
genre_list.append(tag_list)
if self.opts.verbose:
- self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" %
+ if len(genre_list):
+ self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" %
(len(genre_list), len(self.booksByTitle)))
- for genre in genre_list:
- for key in genre:
- self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key),
- len(genre[key]),
- 'titles' if len(genre[key]) > 1 else 'title'))
+ for genre in genre_list:
+ for key in genre:
+ self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key),
+ len(genre[key]),
+ 'titles' if len(genre[key]) > 1 else 'title'))
+
# Write the results
# genre_list = [ {friendly_tag:[{book},{book}]}, {friendly_tag:[{book},{book}]}, ...]
@@ -3105,13 +3109,15 @@ def generateNCXHeader(self):
navPointTag.insert(1, contentTag)
elif self.opts.generate_genres:
contentTag = Tag(soup, 'content')
- contentTag['src'] = "content/ByGenres.html"
+ #contentTag['src'] = "content/ByGenres.html"
+ contentTag['src'] = "%s" % self.genres[0]['file']
navPointTag.insert(1, contentTag)
elif self.opts.generate_recently_added:
contentTag = Tag(soup, 'content')
contentTag['src'] = "content/ByDateAdded.html"
navPointTag.insert(1, contentTag)
else:
+ # Descriptions only
sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \
else self.booksByTitle
contentTag = Tag(soup, 'content')
@@ -3125,7 +3131,6 @@ def generateNCXHeader(self):
navMapTag.insert(0,navPointTag)
ncx.insert(0,navMapTag)
-
self.ncxSoup = soup
def generateNCXDescriptions(self, tocTitle):
@@ -3911,7 +3916,6 @@ def generateNCXByGenre(self, tocTitle):
# Add this section to the body
body.insert(btc, navPointTag)
btc += 1
-
self.ncxSoup = ncx_soup
def writeNCX(self):
@@ -4055,12 +4059,34 @@ def filterDbTags(self, tags):
# Remove the special marker tags from the database's tag list,
# return sorted list of normalized genre tags
+ def format_tag_list(tags, indent=5, line_break=70, header='Tag list'):
+ def next_tag(sorted_tags):
+ for (i, tag) in enumerate(sorted_tags):
+ if i < len(tags) - 1:
+ yield tag + ", "
+ else:
+ yield tag
+
+ ans = '%s%d %s:\n' % (' ' * indent, len(tags), header)
+ ans += ' ' * (indent + 1)
+ out_str = ''
+ sorted_tags = sorted(tags)
+ for tag in next_tag(sorted_tags):
+ out_str += tag
+ if len(out_str) >= line_break:
+ ans += out_str + '\n'
+ out_str = ' ' * (indent + 1)
+ return ans + out_str
+
normalized_tags = []
friendly_tags = []
+ excluded_tags = []
for tag in tags:
- if tag[0] in self.markerTags:
+ if tag in self.markerTags:
+ excluded_tags.append(tag)
continue
if re.search(self.opts.exclude_genre, tag):
+ excluded_tags.append(tag)
continue
if tag == ' ':
continue
@@ -4079,32 +4105,8 @@ def filterDbTags(self, tags):
if genre_tags_dict[key] == normalized:
self.opts.log.warn(" %s" % key)
if self.verbose:
- def next_tag(tags):
- for (i, tag) in enumerate(tags):
- if i < len(tags) - 1:
- yield tag + ", "
- else:
- yield tag
-
- self.opts.log.info(u' %d genre tags in database (excluding genres matching %s):' % \
- (len(genre_tags_dict), self.opts.exclude_genre))
-
- # Display friendly/normalized genres
- # friendly => normalized
- if False:
- sorted_tags = ['%s => %s' % (key, genre_tags_dict[key]) for key in sorted(genre_tags_dict.keys())]
- for tag in next_tag(sorted_tags):
- self.opts.log(u' %s' % tag)
- else:
- sorted_tags = ['%s' % (key) for key in sorted(genre_tags_dict.keys())]
- out_str = ''
- line_break = 70
- for tag in next_tag(sorted_tags):
- out_str += tag
- if len(out_str) >= line_break:
- self.opts.log.info(' %s' % out_str)
- out_str = ''
- self.opts.log.info(' %s' % out_str)
+ self.opts.log.info('%s' % format_tag_list(genre_tags_dict, header="enabled genre tags in database"))
+ self.opts.log.info('%s' % format_tag_list(excluded_tags, header="excluded genre tags"))
return genre_tags_dict
@@ -4969,7 +4971,13 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
else:
opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***')
return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"]
- build_log.append(u" Sections: %s" % ', '.join(sections_list))
+ if opts.fmt == 'mobi' and sections_list == ['Descriptions']:
+ warning = _("\n*** Adding 'By Authors' Section required for MOBI output ***")
+ opts.log.warn(warning)
+ sections_list.insert(0,'Authors')
+ opts.generate_authors = True
+
+ opts.log(u" Sections: %s" % ', '.join(sections_list))
opts.section_list = sections_list
# Limit thumb_width to 1.0" - 2.0"
@@ -5017,7 +5025,7 @@ def run(self, path_to_output, opts, db, notification=DummyReporter()):
if catalog_source_built:
log.info(" Completed catalog source generation\n")
else:
- log.warn(" *** Errors during catalog generation, check log for details ***")
+ log.error(" *** Terminated catalog generation, check log for details ***")
if catalog_source_built:
recommendations = []
From 9f133b37ccf098b4ddb79e17ca04d1311f175526 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 09:05:48 -0700
Subject: [PATCH 32/61] ...
---
src/calibre/gui2/ui.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 6c6e41e0a5..c6d069cc86 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -449,7 +449,7 @@ def library_moved(self, newloc, copy_structure=False):
def set_window_title(self):
- self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
+ self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name())
def location_selected(self, location):
'''
From ad521ab6d3a5cdfc78118151244b6107d870b360 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 09:55:36 -0700
Subject: [PATCH 33/61] cover manip controls
---
src/calibre/gui2/metadata/single.py | 108 +++++++++++++++++++++++++++-
1 file changed, 106 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index b9fae51789..1575702918 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -10,10 +10,10 @@
from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
- QSplitter
+ QSplitter, QPushButton, QGroupBox, QHBoxLayout
from calibre.gui2 import ResizableDialog, file_icon_provider, \
- choose_files, error_dialog
+ choose_files, error_dialog, choose_images
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
@@ -569,9 +569,99 @@ class Cover(ImageView):
def __init__(self, parent):
ImageView.__init__(self, parent)
+ self.dialog = parent
self._cdata = None
self.cover_changed.connect(self.set_pixmap_from_data)
+ self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
+ _('&Browse'), parent)
+ self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
+ _('T&rim'), parent)
+ self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
+ _('&Remove'), parent)
+
+ self.select_cover_button.clicked.connect(self.select_cover)
+ self.remove_cover_button.clicked.connect(self.remove_cover)
+ self.trim_cover_button.clicked.connect(self.trim_cover)
+
+ self.download_cover_button = QPushButton(_('Download co&ver'), parent)
+ self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
+
+ self.download_cover_button.clicked.connect(self.download_cover)
+ self.generate_cover_button.clicked.connect(self.generate_cover)
+
+ self.buttons = [self.select_cover_button, self.remove_cover_button,
+ self.trim_cover_button, self.download_cover_button,
+ self.generate_cover_button]
+
+ def select_cover(self, *args):
+ files = choose_images(self, 'change cover dialog',
+ _('Choose cover for ') +
+ self.dialog.title.current_val)
+ if not files:
+ return
+ _file = files[0]
+ if _file:
+ _file = os.path.abspath(_file)
+ if not os.access(_file, os.R_OK):
+ d = error_dialog(self, _('Cannot read'),
+ _('You do not have permission to read the file: ') + _file)
+ d.exec_()
+ return
+ cf, cover = None, None
+ try:
+ cf = open(_file, "rb")
+ cover = cf.read()
+ except IOError, e:
+ d = error_dialog(self, _('Error reading file'),
+ _("There was an error reading from file:
")
+ + _file + "
"+str(e))
+ d.exec_()
+ if cover:
+ orig = self.current_val
+ self.current_val = cover
+ if self.current_val is None:
+ self.current_val = orig
+ error_dialog(self,
+ _("Not a valid picture"),
+ _file + _(" is not a valid picture"), show=True)
+
+ def remove_cover(self, *args):
+ self.current_val = None
+
+ def trim_cover(self, *args):
+ from calibre.utils.magick import Image
+ cdata = self.current_val
+ if not cdata:
+ return
+ im = Image()
+ im.load(cdata)
+ im.trim(10)
+ cdata = im.export('png')
+ self.current_val = cdata
+
+ def download_cover(self, *args):
+ pass # TODO: Implement this
+
+ def generate_cover(self, *args):
+ from calibre.ebooks import calibre_cover
+ from calibre.ebooks.metadata import fmt_sidx
+ from calibre.gui2 import config
+ title = self.dialog.title.current_val
+ author = authors_to_string(self.dialog.authors.current_val)
+ if not title or not author:
+ return error_dialog(self, _('Specify title and author'),
+ _('You must specify a title and author before generating '
+ 'a cover'), show=True)
+ series = self.dialog.series.current_val
+ series_string = None
+ if series:
+ series_string = _('Book %s of %s')%(
+ fmt_sidx(self.dialog.series_index.current_val,
+ use_roman=config['use_roman_numerals_for_series_number']), series)
+ self.current_val = calibre_cover(title, author,
+ series_string=series_string)
+
def set_pixmap_from_data(self, data):
if not data:
self.current_val = None
@@ -747,6 +837,20 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
self.splitter = QSplitter(Qt.Horizontal, self)
self.splitter.addWidget(self.cover)
l.addWidget(self.splitter)
+ self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
+ gb.l = l = QGridLayout()
+ gb.setLayout(l)
+ for i, b in enumerate(self.cover.buttons[:3]):
+ l.addWidget(b, 0, i, 1, 1)
+ gb.hl = QHBoxLayout()
+ for b in self.cover.buttons[3:]:
+ gb.hl.addWidget(b)
+ l.addLayout(gb.hl, 1, 0, 1, 3)
+ self.tabs[0].middle = w = QWidget(self)
+ w.l = l = QGridLayout()
+ w.setLayout(w.l)
+ l.addWidget(gb, 0, 0, 1, 3)
+ self.splitter.addWidget(w)
# }}}
def __call__(self, id_, has_next=False, has_previous=False):
From cdee30ffd39cd25746810b1af5ed341c54eb675e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 10:26:02 -0700
Subject: [PATCH 34/61] And we have comments
---
src/calibre/gui2/metadata/single.py | 38 +++++++++++++++++++++++++++--
1 file changed, 36 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 1575702918..9cffe2ee55 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -25,7 +25,8 @@
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import
from calibre.utils.date import utcfromtimestamp
-
+from calibre.gui2.comments_editor import Editor
+from calibre.library.comments import comments_to_html
'''
The interface common to all widgets used to set basic metadata
@@ -565,7 +566,7 @@ def show_format(self, item, *args):
# }}}
-class Cover(ImageView):
+class Cover(ImageView): # {{{
def __init__(self, parent):
ImageView.__init__(self, parent)
@@ -713,7 +714,30 @@ def commit(self, db, id_):
db.remove_cover(id_, notify=False, commit=False)
return True
+# }}}
+class CommentsEdit(Editor): # {{{
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.html
+ def fset(self, val):
+ if not val or not val.strip():
+ val = ''
+ else:
+ val = comments_to_html(val)
+ self.html = val
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.current_val = db.comments(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_comment(id_, self.current_val, notify=False, commit=False)
+ return True
+# }}}
class MetadataSingleDialog(ResizableDialog):
@@ -799,6 +823,9 @@ def create_basic_metadata_widgets(self): # {{{
self.cover = Cover(self)
self.basic_metadata_widgets.append(self.cover)
+ self.comments = CommentsEdit(self)
+ self.basic_metadata_widgets.append(self.comments)
+
# }}}
def do_layout(self): # {{{
@@ -851,6 +878,13 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
w.setLayout(w.l)
l.addWidget(gb, 0, 0, 1, 3)
self.splitter.addWidget(w)
+
+ self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
+ gb.l = l = QVBoxLayout()
+ gb.setLayout(l)
+ l.addWidget(self.comments)
+ self.splitter.addWidget(gb)
+
# }}}
def __call__(self, id_, has_next=False, has_previous=False):
From a6bded4378460a6443f92e2d07719d63855a32c0 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 11:11:26 -0700
Subject: [PATCH 35/61] Fix #8475 (calibre not finding device)
---
src/calibre/devices/android/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 277070020b..1b5cbe4bed 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -21,7 +21,7 @@ class ANDROID(USBMS):
# HTC
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100, 0x0227], 0x0ff9
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226],
- 0xc92 : [0x100], 0xc97: [0x226]},
+ 0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]},
# Eken
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
From f573d07e819fe6f557465dd94712492774520ba9 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 11:16:03 -0700
Subject: [PATCH 36/61] And we have tags
---
src/calibre/gui2/dialogs/metadata_single.py | 2 +-
src/calibre/gui2/dialogs/tag_editor.py | 4 +-
src/calibre/gui2/metadata/single.py | 135 +++++++++++++++++++-
src/calibre/manual/faq.rst | 2 +-
4 files changed, 133 insertions(+), 10 deletions(-)
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index a2ced18e0f..00bc98cb17 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -775,7 +775,7 @@ def edit_tags(self):
self.original_tags = unicode(self.tags.text())
else:
self.tags.setText(self.original_tags)
- d = TagEditor(self, self.db, self.row)
+ d = TagEditor(self, self.db, self.id)
d.exec_()
if d.result() == QDialog.Accepted:
tag_string = ', '.join(d.tags)
diff --git a/src/calibre/gui2/dialogs/tag_editor.py b/src/calibre/gui2/dialogs/tag_editor.py
index 6c5aa6de66..c12b7357f1 100644
--- a/src/calibre/gui2/dialogs/tag_editor.py
+++ b/src/calibre/gui2/dialogs/tag_editor.py
@@ -10,13 +10,13 @@
class TagEditor(QDialog, Ui_TagEditor):
- def __init__(self, window, db, index=None):
+ def __init__(self, window, db, id_=None):
QDialog.__init__(self, window)
Ui_TagEditor.__init__(self)
self.setupUi(self)
self.db = db
- self.index = index
+ self.index = db.row(id_)
if self.index is not None:
tags = self.db.tags(self.index)
else:
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 9cffe2ee55..f531e62fde 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -10,14 +10,15 @@
from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
- QSplitter, QPushButton, QGroupBox, QHBoxLayout
+ QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \
+ QMessageBox
from calibre.gui2 import ResizableDialog, file_icon_provider, \
- choose_files, error_dialog, choose_images
+ choose_files, error_dialog, choose_images, question_dialog
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox, FormatList, ImageView
+ EnComboBox, FormatList, ImageView, CompleteLineEdit
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors
from calibre.utils.date import local_tz
@@ -27,6 +28,7 @@
from calibre.utils.date import utcfromtimestamp
from calibre.gui2.comments_editor import Editor
from calibre.library.comments import comments_to_html
+from calibre.gui2.dialogs.tag_editor import TagEditor
'''
The interface common to all widgets used to set basic metadata
@@ -739,6 +741,104 @@ def commit(self, db, id_):
return True
# }}}
+class RatingEdit(QSpinBox): # {{{
+ LABEL = _('&Rating:')
+ TOOLTIP = _('Rating of this book. 0-5 stars')
+
+ def __init__(self, parent):
+ QSpinBox.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setMaximum(5)
+ self.setSuffix(' ' + _('stars'))
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.value()
+ def fset(self, val):
+ if val is None:
+ val = 0
+ val = int(val)
+ if val < 0:
+ val = 0
+ if val > 5:
+ val = 5
+ self.setValue(val)
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ val = db.rating(id_, index_is_id=True)
+ if val > 0:
+ val = int(val/2.)
+ else:
+ val = 0
+ self.current_val = val
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
+class TagsEdit(CompleteLineEdit): # {{{
+ LABEL = _('Ta&gs:')
+ TOOLTIP = ''+_('Tags categorize the book. This is particularly '
+ 'useful while searching.
They can be any words'
+ 'or phrases, separated by commas.')
+
+ def __init__(self, parent):
+ CompleteLineEdit.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return [x.strip() for x in unicode(self.text()).split(',')]
+ def fset(self, val):
+ if not val:
+ val = []
+ self.setText(', '.join([x.strip() for x in val]))
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ tags = db.tags(id_, index_is_id=True)
+ tags = tags.split(',') if tags else []
+ self.current_val = tags
+ self.update_items_cache(db.all_tags())
+ self.original_val = self.current_val
+
+ @property
+ def changed(self):
+ return self.current_val != self.original_val
+
+ def edit(self, db, id_):
+ if self.changed:
+ if question_dialog(self, _('Tags changed'),
+ _('You have changed the tags. In order to use the tags'
+ ' editor, you must either discard or apply these '
+ 'changes'), show_copy_button=False,
+ buttons=QMessageBox.Apply|QMessageBox.Discard,
+ yes_button=QMessageBox.Apply):
+ self.commit(db, id_)
+ db.commit()
+ self.original_val = self.current_val
+ else:
+ self.current_val = self.original_val
+ d = TagEditor(self, db, id_)
+ if d.exec_() == TagEditor.Accepted:
+ self.current_val = d.tags
+ self.update_items_cache(db.all_tags())
+
+
+ def commit(self, db, id_):
+ db.set_tags(id_, self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
class MetadataSingleDialog(ResizableDialog):
view_format = pyqtSignal(object)
@@ -778,7 +878,7 @@ def setupUi(self, *args): # {{{
def create_basic_metadata_widgets(self): # {{{
self.basic_metadata_widgets = []
- # Title
+
self.title = TitleEdit(self)
self.deduce_title_sort_button = QToolButton(self)
self.deduce_title_sort_button.setToolTip(
@@ -791,7 +891,6 @@ def create_basic_metadata_widgets(self): # {{{
self.deduce_title_sort_button)
self.basic_metadata_widgets.extend([self.title, self.title_sort])
- # Authors
self.authors = AuthorsEdit(self)
self.deduce_author_sort_button = QToolButton(self)
self.deduce_author_sort_button.setToolTip(_(
@@ -826,6 +925,17 @@ def create_basic_metadata_widgets(self): # {{{
self.comments = CommentsEdit(self)
self.basic_metadata_widgets.append(self.comments)
+ self.rating = RatingEdit(self)
+ self.basic_metadata_widgets.append(self.rating)
+
+ self.tags = TagsEdit(self)
+ self.tags_editor_button = QToolButton(self)
+ self.tags_editor_button.setToolTip(_('Open Tag Editor'))
+ self.tags_editor_button.setIcon(QIcon(I('chapters.png')))
+ self.tags_editor_button.clicked.connect(self.tags_editor)
+ self.basic_metadata_widgets.append(self.tags)
+
+
# }}}
def do_layout(self): # {{{
@@ -876,8 +986,18 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
self.tabs[0].middle = w = QWidget(self)
w.l = l = QGridLayout()
w.setLayout(w.l)
- l.addWidget(gb, 0, 0, 1, 3)
+ l.setMargin(0)
self.splitter.addWidget(w)
+ def create_row2(row, widget, button=None):
+ ql = BuddyLabel(widget)
+ l.addWidget(ql, row, 0, 1, 1)
+ l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
+ if button is not None:
+ l.addWidget(button, row, 2, 1, 1)
+
+ l.addWidget(gb, 0, 0, 1, 3)
+ create_row2(1, self.rating)
+ create_row2(2, self.tags, self.tags_editor_button)
self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
gb.l = l = QVBoxLayout()
@@ -911,6 +1031,9 @@ def remove_unused_series(self, *args):
self.series.setCurrentIndex(i)
break
+ def tags_editor(self, *args):
+ self.tags.edit(self.db, self.book_id)
+
if __name__ == '__main__':
from PyQt4.Qt import QApplication
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 3e382c8f10..5ebe91bc76 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -441,7 +441,7 @@ menu, choose "Validate fonts".
I downloaded the installer, but it is not working?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums `_.
+Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Try rebooting your computer and running a registry cleaner like `Wise registry cleaner `_. Best place to ask for more help is in the `forums `_.
My antivirus program claims |app| is a virus/trojan?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 1820832fa8ae0bcb89c800595de9adc4ed52164b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 11:59:19 -0700
Subject: [PATCH 37/61] Fix #8477 (Series/Sequence Info no longer being
downloaded)
---
src/calibre/__init__.py | 8 +++++---
src/calibre/ebooks/metadata/library_thing.py | 21 ++++++++++++++++----
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py
index a4f7439405..221f5911c6 100644
--- a/src/calibre/__init__.py
+++ b/src/calibre/__init__.py
@@ -241,7 +241,7 @@ def get_parsed_proxy(typ='http', debug=True):
return ans
-def browser(honor_time=True, max_time=2, mobile_browser=False):
+def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
'''
Create a mechanize browser for web scraping. The browser handles cookies,
refresh requests and ignores robots.txt. Also uses proxy if avaialable.
@@ -253,8 +253,10 @@ def browser(honor_time=True, max_time=2, mobile_browser=False):
opener = Browser()
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
opener.set_handle_robots(False)
- opener.addheaders = [('User-agent', ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
- 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13')]
+ if user_agent is None:
+ user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
+ 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
+ opener.addheaders = [('User-agent', user_agent)]
http_proxy = get_proxies().get('http', None)
if http_proxy:
opener.set_proxies({'http':http_proxy})
diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py
index 7f312da1d9..d956747a2b 100644
--- a/src/calibre/ebooks/metadata/library_thing.py
+++ b/src/calibre/ebooks/metadata/library_thing.py
@@ -4,7 +4,7 @@
Fetch cover from LibraryThing.com based on ISBN number.
'''
-import sys, socket, os, re
+import sys, socket, os, re, random
from lxml import html
import mechanize
@@ -16,13 +16,26 @@
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
+def get_ua():
+ choices = [
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
+ 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
+ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'
+ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)'
+ 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16'
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19'
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11'
+ ]
+ return choices[random.randint(0, len(choices)-1)]
+
+
class HeadRequest(mechanize.Request):
def get_method(self):
return 'HEAD'
def check_for_cover(isbn, timeout=5.):
- br = browser()
+ br = browser(user_agent=get_ua())
br.set_handle_redirect(False)
try:
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
@@ -51,7 +64,7 @@ def login(br, username, password, force=True):
def cover_from_isbn(isbn, timeout=5., username=None, password=None):
src = None
- br = browser()
+ br = browser(user_agent=get_ua())
try:
return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
except:
@@ -100,7 +113,7 @@ def get_social_metadata(title, authors, publisher, isbn, username=None,
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(title, authors)
if isbn:
- br = browser()
+ br = browser(user_agent=get_ua())
if username and password:
try:
login(br, username, password, force=False)
From 5f9fcaa1882436f3bd995bf330e5281048c3db58 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 13:15:53 -0700
Subject: [PATCH 38/61] Fix #8479 (Updated recipe for Blic)
---
resources/recipes/blic.recipe | 44 ++++++++++++++++++++++++++++++-----
1 file changed, 38 insertions(+), 6 deletions(-)
diff --git a/resources/recipes/blic.recipe b/resources/recipes/blic.recipe
index 0c955bebde..384518ec13 100644
--- a/resources/recipes/blic.recipe
+++ b/resources/recipes/blic.recipe
@@ -1,6 +1,6 @@
__license__ = 'GPL v3'
-__copyright__ = '2008-2010, Darko Miletic '
+__copyright__ = '2008-2011, Darko Miletic '
'''
blic.rs
'''
@@ -21,21 +21,53 @@ class Blic(BasicNewsRecipe):
masthead_url = 'http://www.blic.rs/resources/images/header/header_back.png'
language = 'sr'
publication_type = 'newspaper'
- extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Georgia, serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} .img_full{float: none} img{margin-bottom: 0.8em} '
+ extra_css = """
+ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
+ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
+ body{font-family: Georgia, serif1, serif}
+ .articledescription,#nadnaslov,.article_info{font-family: Arial, sans1, sans-serif}
+ .img_full{float: none}
+ #nadnaslov{font-size: small}
+ #article_lead{font-size: 1.5em}
+ h1{color: red}
+ .potpis{font-size: x-small; color: gray}
+ .article_info{font-size: small}
+ img{margin-bottom: 0.8em; margin-top: 0.8em; display: block}
+ """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher': publisher
, 'language' : language
+ , 'linearize_tables' : True
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
remove_tags_before = dict(name='div', attrs={'id':'article_info'})
- remove_tags = [dict(name=['object','link'])]
- remove_attributes = ['width','height']
+ remove_tags = [dict(name=['object','link','meta','base','object','embed'])]
+ remove_attributes = ['width','height','m_id','m_ext','mlg_id','poll_id','v_id']
- feeds = [(u'Danasnje Vesti', u'http://www.blic.rs/rss/danasnje-vesti')]
+ feeds = [
+ (u'Politika' , u'http://www.blic.rs/rss/Vesti/Politika')
+ ,(u'Tema Dana' , u'http://www.blic.rs/rss/Vesti/Tema-Dana')
+ ,(u'Svet' , u'http://www.blic.rs/rss/Vesti/Svet')
+ ,(u'Drustvo' , u'http://www.blic.rs/rss/Vesti/Drustvo')
+ ,(u'Ekonomija' , u'http://www.blic.rs/rss/Vesti/Ekonomija')
+ ,(u'Hronika' , u'http://www.blic.rs/rss/Vesti/Hronika')
+ ,(u'Beograd' , u'http://www.blic.rs/rss/Vesti/Beograd')
+ ,(u'Srbija' , u'http://www.blic.rs/rss/Vesti/Srbija')
+ ,(u'Vojvodina' , u'http://www.blic.rs/rss/Vesti/Vojvodina')
+ ,(u'Republika Srpska' , u'http://www.blic.rs/rss/Vesti/Republika-Srpska')
+ ,(u'Reportaza' , u'http://www.blic.rs/rss/Vesti/Reportaza')
+ ,(u'Dodatak' , u'http://www.blic.rs/rss/Vesti/Dodatak')
+ ,(u'Zabava' , u'http://www.blic.rs/rss/Zabava')
+ ,(u'Kultura' , u'http://www.blic.rs/rss/Kultura')
+ ,(u'Slobodno Vreme' , u'http://www.blic.rs/rss/Slobodno-vreme')
+ ,(u'IT' , u'http://www.blic.rs/rss/IT')
+ ,(u'Komentar' , u'http://www.blic.rs/rss/Komentar')
+ ,(u'Intervju' , u'http://www.blic.rs/rss/Intervju')
+ ]
def print_version(self, url):
@@ -44,4 +76,4 @@ def print_version(self, url):
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
- return self.adeify_images(soup)
+ return soup
From cf4e47fcf8d7304ed66684f245df9aa460714fde Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 14:56:56 -0700
Subject: [PATCH 39/61] isbn added
---
src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index f531e62fde..daae579334 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -11,7 +11,7 @@
QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \
- QMessageBox
+ QMessageBox, QLineEdit
from calibre.gui2 import ResizableDialog, file_icon_provider, \
choose_files, error_dialog, choose_images, question_dialog
@@ -20,7 +20,7 @@
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
EnComboBox, FormatList, ImageView, CompleteLineEdit
from calibre.ebooks.metadata import title_sort, authors_to_string, \
- string_to_authors
+ string_to_authors, check_isbn
from calibre.utils.date import local_tz
from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS
@@ -839,6 +839,47 @@ def commit(self, db, id_):
# }}}
+class ISBNEdit(QLineEdit): # {{{
+ LABEL = _('IS&BN:')
+
+ def __init__(self, parent):
+ QLineEdit.__init__(self, parent)
+ self.pat = re.compile(r'[^0-9a-zA-Z]')
+ self.textChanged.connect(self.validate)
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.pat.sub('', unicode(self.text()).strip())
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setText(val.strip())
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.current_val = db.isbn(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_isbn(id_, self.current_val, notify=False, commit=False)
+ return True
+
+ def validate(self, *args):
+ isbn = self.current_val
+ tt = _('This ISBN number is valid')
+ if not isbn:
+ col = 'rgba(0,255,0,0%)'
+ elif check_isbn(isbn) is not None:
+ col = 'rgba(0,255,0,20%)'
+ else:
+ col = 'rgba(255,0,0,20%)'
+ tt = _('This ISBN number is invalid')
+ self.setToolTip(tt)
+ self.setStyleSheet('QLineEdit { background-color: %s }'%col)
+
+# }}}
+
class MetadataSingleDialog(ResizableDialog):
view_format = pyqtSignal(object)
@@ -935,6 +976,8 @@ def create_basic_metadata_widgets(self): # {{{
self.tags_editor_button.clicked.connect(self.tags_editor)
self.basic_metadata_widgets.append(self.tags)
+ self.isbn = ISBNEdit(self)
+ self.basic_metadata_widgets.append(self.isbn)
# }}}
@@ -998,6 +1041,7 @@ def create_row2(row, widget, button=None):
l.addWidget(gb, 0, 0, 1, 3)
create_row2(1, self.rating)
create_row2(2, self.tags, self.tags_editor_button)
+ create_row2(3, self.isbn)
self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
gb.l = l = QVBoxLayout()
From daeaa718123a75157b2053d0b95966ebbfdf5245 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 15:04:40 -0700
Subject: [PATCH 40/61] publisher added
---
src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index daae579334..de59e8075d 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -880,6 +880,50 @@ def validate(self, *args):
# }}}
+class PublisherEdit(EnComboBox): # {{{
+ LABEL = _('&Publisher:')
+
+ def __init__(self, parent):
+ EnComboBox.__init__(self, parent)
+ self.setSizeAdjustPolicy(
+ self.AdjustToMinimumContentsLengthWithIcon)
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.currentText()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setEditText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ all_publishers = db.all_publishers()
+ all_publishers.sort(key=lambda x : sort_key(x[1]))
+ publisher_id = db.publisher_id(id_, index_is_id=True)
+ idx, c = None, 0
+ for i in all_publishers:
+ id, name = i
+ if id == publisher_id:
+ idx = c
+ self.addItem(name)
+ c += 1
+
+ self.setEditText('')
+ if idx is not None:
+ self.setCurrentIndex(idx)
+
+ def commit(self, db, id_):
+ db.set_publisher(id_, self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
class MetadataSingleDialog(ResizableDialog):
view_format = pyqtSignal(object)
@@ -979,6 +1023,9 @@ def create_basic_metadata_widgets(self): # {{{
self.isbn = ISBNEdit(self)
self.basic_metadata_widgets.append(self.isbn)
+ self.publisher = PublisherEdit(self)
+ self.basic_metadata_widgets.append(self.publisher)
+
# }}}
def do_layout(self): # {{{
@@ -1042,6 +1089,7 @@ def create_row2(row, widget, button=None):
create_row2(1, self.rating)
create_row2(2, self.tags, self.tags_editor_button)
create_row2(3, self.isbn)
+ create_row2(4, self.publisher)
self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
gb.l = l = QVBoxLayout()
From 631bd5d63d671ad73d912e37ec6e4e045366c217 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 15:14:17 -0700
Subject: [PATCH 41/61] ...
---
src/calibre/gui2/metadata/basic_widgets.py | 927 +++++++++++++++++++++
src/calibre/gui2/metadata/single.py | 925 +-------------------
2 files changed, 936 insertions(+), 916 deletions(-)
create mode 100644 src/calibre/gui2/metadata/basic_widgets.py
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
new file mode 100644
index 0000000000..eb162ac9d5
--- /dev/null
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -0,0 +1,927 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import textwrap, re, os
+
+from PyQt4.Qt import Qt, \
+ QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
+ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
+ QPushButton, QSpinBox, \
+ QMessageBox, QLineEdit
+
+from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
+ EnComboBox, FormatList, ImageView, CompleteLineEdit
+from calibre.utils.icu import sort_key
+from calibre.utils.config import tweaks
+from calibre.ebooks.metadata import title_sort, authors_to_string, \
+ string_to_authors, check_isbn
+from calibre.gui2 import file_icon_provider, \
+ choose_files, error_dialog, choose_images, question_dialog
+from calibre.utils.date import local_tz
+from calibre import strftime
+from calibre.ebooks import BOOK_EXTENSIONS
+from calibre.customize.ui import run_plugins_on_import
+from calibre.utils.date import utcfromtimestamp
+from calibre.gui2.comments_editor import Editor
+from calibre.library.comments import comments_to_html
+from calibre.gui2.dialogs.tag_editor import TagEditor
+
+'''
+The interface common to all widgets used to set basic metadata
+class BasicMetadataWidget(object):
+
+ LABEL = "label text"
+
+ def initialize(self, db, id_):
+ pass
+
+ def commit(self, db, id_):
+ return True
+
+ @dynamic_property
+ def current_val(self):
+ # Present in most but not all basic metadata widgets
+ def fget(self):
+ return None
+ def fset(self, val):
+ pass
+ return property(fget=fget, fset=fset)
+'''
+
+# Title {{{
+class TitleEdit(EnLineEdit):
+
+ TITLE_ATTR = 'title'
+ COMMIT = True
+ TOOLTIP = _('Change the title of this book')
+ LABEL = _('&Title:')
+
+ def __init__(self, parent):
+ self.dialog = parent
+ EnLineEdit.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+
+ def get_default(self):
+ return _('Unknown')
+
+ def initialize(self, db, id_):
+ title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
+ self.current_val = title
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ title = self.current_val
+ if self.COMMIT:
+ getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
+ else:
+ getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
+ commit=False)
+ return True
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ title = unicode(self.text()).strip()
+ if not title:
+ title = self.get_default()
+ return title
+
+ def fset(self, val):
+ if hasattr(val, 'strip'):
+ val = val.strip()
+ if not val:
+ val = self.get_default()
+ self.setText(val)
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+class TitleSortEdit(TitleEdit):
+
+ TITLE_ATTR = 'title_sort'
+ COMMIT = False
+ TOOLTIP = _('Specify how this book should be sorted when by title.'
+ ' For example, The Exorcist might be sorted as Exorcist, The.')
+ LABEL = _('Title &sort:')
+
+ def __init__(self, parent, title_edit, autogen_button):
+ TitleEdit.__init__(self, parent)
+ self.title_edit = title_edit
+
+ base = self.TOOLTIP
+ ok_tooltip = '' + textwrap.fill(base+'
'+
+ _(' The green color indicates that the current '
+ 'title sort matches the current title'))
+ bad_tooltip = '
'+textwrap.fill(base + '
'+
+ _(' The red color warns that the current '
+ 'title sort does not match the current title. '
+ 'No action is required if this is what you want.'))
+ self.tooltips = (ok_tooltip, bad_tooltip)
+
+ self.title_edit.textChanged.connect(self.update_state)
+ self.textChanged.connect(self.update_state)
+
+ autogen_button.clicked.connect(self.auto_generate)
+ self.update_state()
+
+ def update_state(self, *args):
+ ts = title_sort(self.title_edit.current_val)
+ normal = ts == self.current_val
+ if normal:
+ col = 'rgb(0, 255, 0, 20%)'
+ else:
+ col = 'rgb(255, 0, 0, 20%)'
+ self.setStyleSheet('QLineEdit { color: black; '
+ 'background-color: %s; }'%col)
+ tt = self.tooltips[0 if normal else 1]
+ self.setToolTip(tt)
+ self.setWhatsThis(tt)
+
+ def auto_generate(self, *args):
+ self.current_val = title_sort(self.title_edit.current_val)
+
+# }}}
+
+# Authors {{{
+class AuthorsEdit(CompleteComboBox):
+
+ TOOLTIP = ''
+ LABEL = _('&Author(s):')
+
+ def __init__(self, parent):
+ self.dialog = parent
+ CompleteComboBox.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setEditable(True)
+ self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
+
+ def get_default(self):
+ return _('Unknown')
+
+ def initialize(self, db, id_):
+ all_authors = db.all_authors()
+ all_authors.sort(key=lambda x : sort_key(x[1]))
+ for i in all_authors:
+ id, name = i
+ name = [name.strip().replace('|', ',') for n in name.split(',')]
+ self.addItem(authors_to_string(name))
+
+ self.set_separator('&')
+ self.set_space_before_sep(True)
+ self.update_items_cache(db.all_author_names())
+
+ au = db.authors(id_, index_is_id=True)
+ if not au:
+ au = _('Unknown')
+ self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ authors = self.current_val
+ db.set_authors(id_, authors, notify=False)
+ return True
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ au = unicode(self.text()).strip()
+ if not au:
+ au = self.get_default()
+ return string_to_authors(au)
+
+ def fset(self, val):
+ if not val:
+ val = [self.get_default()]
+ self.setEditText(' & '.join([x.strip() for x in val]))
+ self.lineEdit().setCursorPosition(0)
+
+
+ return property(fget=fget, fset=fset)
+
+class AuthorSortEdit(EnLineEdit):
+
+ TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
+ 'For example Charles Dickens should be sorted as Dickens, '
+ 'Charles.\nIf the box is colored green, then text matches '
+ 'the individual author\'s sort strings. If it is colored '
+ 'red, then the authors and this text do not match.')
+ LABEL = _('Author s&ort:')
+
+ def __init__(self, parent, authors_edit, autogen_button, db):
+ EnLineEdit.__init__(self, parent)
+ self.authors_edit = authors_edit
+ self.db = db
+
+ base = self.TOOLTIP
+ ok_tooltip = '
' + textwrap.fill(base+'
'+
+ _(' The green color indicates that the current '
+ 'author sort matches the current author'))
+ bad_tooltip = '
'+textwrap.fill(base + '
'+
+ _(' The red color indicates that the current '
+ 'author sort does not match the current author. '
+ 'No action is required if this is what you want.'))
+ self.tooltips = (ok_tooltip, bad_tooltip)
+
+ self.authors_edit.editTextChanged.connect(self.update_state)
+ self.textChanged.connect(self.update_state)
+
+ autogen_button.clicked.connect(self.auto_generate)
+ self.update_state()
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.text()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def update_state(self, *args):
+ au = unicode(self.authors_edit.text())
+ au = re.sub(r'\s+et al\.$', '', au)
+ au = self.db.author_sort_from_authors(string_to_authors(au))
+
+ normal = au == self.current_val
+ if normal:
+ col = 'rgb(0, 255, 0, 20%)'
+ else:
+ col = 'rgb(255, 0, 0, 20%)'
+ self.setStyleSheet('QLineEdit { color: black; '
+ 'background-color: %s; }'%col)
+ tt = self.tooltips[0 if normal else 1]
+ self.setToolTip(tt)
+ self.setWhatsThis(tt)
+
+ def auto_generate(self, *args):
+ au = unicode(self.authors_edit.text())
+ au = re.sub(r'\s+et al\.$', '', au)
+ authors = string_to_authors(au)
+ self.current_val = self.db.author_sort_from_authors(authors)
+
+ def initialize(self, db, id_):
+ self.current_val = db.author_sort(id_, index_is_id=True)
+
+ def commit(self, db, id_):
+ aus = self.current_val
+ db.set_author_sort(id_, aus, notify=False, commit=False)
+ return True
+
+# }}}
+
+# Series {{{
+class SeriesEdit(EnComboBox):
+
+ TOOLTIP = _('List of known series. You can add new series.')
+ LABEL = _('&Series:')
+
+ def __init__(self, parent):
+ EnComboBox.__init__(self, parent)
+ self.dialog = parent
+ self.setSizeAdjustPolicy(
+ self.AdjustToMinimumContentsLengthWithIcon)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setEditable(True)
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.currentText()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setEditText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ all_series = db.all_series()
+ all_series.sort(key=lambda x : sort_key(x[1]))
+ series_id = db.series_id(id_, index_is_id=True)
+ idx, c = None, 0
+ for i in all_series:
+ id, name = i
+ if id == series_id:
+ idx = c
+ self.addItem(name)
+ c += 1
+
+ self.lineEdit().setText('')
+ if idx is not None:
+ self.setCurrentIndex(idx)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ series = self.current_val
+ db.set_series(id_, series, notify=False, commit=True)
+ return True
+
+class SeriesIndexEdit(QDoubleSpinBox):
+
+ TOOLTIP = ''
+ LABEL = _('&Number:')
+
+ def __init__(self, parent, series_edit):
+ QDoubleSpinBox.__init__(self, parent)
+ self.dialog = parent
+ self.db = self.original_series_name = None
+ self.setMaximum(1000000)
+ self.series_edit = series_edit
+ series_edit.currentIndexChanged.connect(self.enable)
+ series_edit.editTextChanged.connect(self.enable)
+ series_edit.lineEdit().editingFinished.connect(self.increment)
+ self.enable()
+
+ def enable(self, *args):
+ self.setEnabled(bool(self.series_edit.current_val))
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return self.value()
+
+ def fset(self, val):
+ if val is None:
+ val = 1.0
+ val = float(val)
+ self.setValue(val)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.db = db
+ if self.series_edit.current_val:
+ val = db.series_index(id_, index_is_id=True)
+ else:
+ val = 1.0
+ self.current_val = val
+ self.original_val = self.current_val
+ self.original_series_name = self.series_edit.original_val
+
+ def commit(self, db, id_):
+ db.set_series_index(id_, self.current_val, notify=False, commit=False)
+ return True
+
+ def increment(self):
+ if self.db is not None:
+ try:
+ series = self.series_edit.current_val
+ if series and series != self.original_series_name:
+ ns = 1.0
+ if tweaks['series_index_auto_increment'] != 'const':
+ ns = self.db.get_next_series_num_for(series)
+ self.current_val = ns
+ self.original_series_name = series
+ except:
+ import traceback
+ traceback.print_exc()
+
+
+# }}}
+
+class BuddyLabel(QLabel): # {{{
+
+ def __init__(self, buddy):
+ QLabel.__init__(self, buddy.LABEL)
+ self.setBuddy(buddy)
+ self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
+# }}}
+
+class Format(QListWidgetItem): # {{{
+
+ def __init__(self, parent, ext, size, path=None, timestamp=None):
+ self.path = path
+ self.ext = ext
+ self.size = float(size)/(1024*1024)
+ text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
+ QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
+ text, parent, QListWidgetItem.UserType)
+ if timestamp is not None:
+ ts = timestamp.astimezone(local_tz)
+ t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
+ text = _('Last modified: %s')%t
+ self.setToolTip(text)
+ self.setStatusTip(text)
+
+# }}}
+
+class FormatsManager(QWidget): # {{{
+
+ def __init__(self, parent):
+ QWidget.__init__(self, parent)
+ self.dialog = parent
+ self.changed = False
+
+ self.l = l = QGridLayout()
+ self.setLayout(l)
+ self.cover_from_format_button = QToolButton(self)
+ self.cover_from_format_button.setToolTip(
+ _('Set the cover for the book from the selected format'))
+ self.cover_from_format_button.setIcon(QIcon(I('book.png')))
+ self.cover_from_format_button.setIconSize(QSize(32, 32))
+
+ self.metadata_from_format_button = QToolButton(self)
+ self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
+ self.metadata_from_format_button.setIconSize(QSize(32, 32))
+ # TODO: Implement the *_from_format buttons
+
+ self.add_format_button = QToolButton(self)
+ self.add_format_button.setIcon(QIcon(I('add_book.png')))
+ self.add_format_button.setIconSize(QSize(32, 32))
+ self.add_format_button.clicked.connect(self.add_format)
+
+ self.remove_format_button = QToolButton(self)
+ self.remove_format_button.setIcon(QIcon(I('trash.png')))
+ self.remove_format_button.setIconSize(QSize(32, 32))
+ self.remove_format_button.clicked.connect(self.remove_format)
+
+ self.formats = FormatList(self)
+ self.formats.setAcceptDrops(True)
+ self.formats.formats_dropped.connect(self.formats_dropped)
+ self.formats.delete_format.connect(self.remove_format)
+ self.formats.itemDoubleClicked.connect(self.show_format)
+ self.formats.setDragDropMode(self.formats.DropOnly)
+ self.formats.setIconSize(QSize(32, 32))
+ self.formats.setMaximumWidth(200)
+
+ l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
+ l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
+ l.addWidget(self.add_format_button, 0, 2, 1, 1)
+ l.addWidget(self.remove_format_button, 2, 2, 1, 1)
+ l.addWidget(self.formats, 0, 1, 3, 1)
+
+
+
+ def initialize(self, db, id_):
+ self.changed = False
+ exts = db.formats(id_, index_is_id=True)
+ if exts:
+ exts = exts.split(',')
+ for ext in exts:
+ if not ext:
+ ext = ''
+ size = db.sizeof_format(id_, ext, index_is_id=True)
+ timestamp = db.format_last_modified(id_, ext)
+ if size is None:
+ continue
+ Format(self.formats, ext, size, timestamp=timestamp)
+
+ def commit(self, db, id_):
+ if not self.changed:
+ return True
+ old_extensions, new_extensions, paths = set(), set(), {}
+ for row in range(self.formats.count()):
+ fmt = self.formats.item(row)
+ ext, path = fmt.ext.lower(), fmt.path
+ if 'unknown' in ext.lower():
+ ext = None
+ if path:
+ new_extensions.add(ext)
+ paths[ext] = path
+ else:
+ old_extensions.add(ext)
+ for ext in new_extensions:
+ db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
+ index_is_id=True)
+ db_extensions = set([f.lower() for f in db.formats(id_,
+ index_is_id=True).split(',')])
+ extensions = new_extensions.union(old_extensions)
+ for ext in db_extensions:
+ if ext not in extensions:
+ db.remove_format(id_, ext, notify=False, index_is_id=True)
+
+ self.changed = False
+ return True
+
+ def add_format(self, *args):
+ files = choose_files(self, 'add formats dialog',
+ _("Choose formats for ") +
+ self.dialog.title.current_val,
+ [(_('Books'), BOOK_EXTENSIONS)])
+ self._add_formats(files)
+
+ def _add_formats(self, paths):
+ added = False
+ if not paths:
+ return added
+ bad_perms = []
+ for _file in paths:
+ _file = os.path.abspath(_file)
+ if not os.access(_file, os.R_OK):
+ bad_perms.append(_file)
+ continue
+
+ nfile = run_plugins_on_import(_file)
+ if nfile is not None:
+ _file = nfile
+ stat = os.stat(_file)
+ size = stat.st_size
+ ext = os.path.splitext(_file)[1].lower().replace('.', '')
+ timestamp = utcfromtimestamp(stat.st_mtime)
+ for row in range(self.formats.count()):
+ fmt = self.formats.item(row)
+ if fmt.ext.lower() == ext:
+ self.formats.takeItem(row)
+ break
+ Format(self.formats, ext, size, path=_file, timestamp=timestamp)
+ self.changed = True
+ added = True
+ if bad_perms:
+ error_dialog(self, _('No permission'),
+ _('You do not have '
+ 'permission to read the following files:'),
+ det_msg='\n'.join(bad_perms), show=True)
+
+ return added
+
+ def formats_dropped(self, event, paths):
+ if self._add_formats(paths):
+ event.accept()
+
+ def remove_format(self, *args):
+ rows = self.formats.selectionModel().selectedRows(0)
+ for row in rows:
+ self.formats.takeItem(row.row())
+ self.changed = True
+
+ def show_format(self, item, *args):
+ fmt = item.ext
+ self.dialog.view_format.emit(fmt)
+
+# }}}
+
+class Cover(ImageView): # {{{
+
+ def __init__(self, parent):
+ ImageView.__init__(self, parent)
+ self.dialog = parent
+ self._cdata = None
+ self.cover_changed.connect(self.set_pixmap_from_data)
+
+ self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
+ _('&Browse'), parent)
+ self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
+ _('T&rim'), parent)
+ self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
+ _('&Remove'), parent)
+
+ self.select_cover_button.clicked.connect(self.select_cover)
+ self.remove_cover_button.clicked.connect(self.remove_cover)
+ self.trim_cover_button.clicked.connect(self.trim_cover)
+
+ self.download_cover_button = QPushButton(_('Download co&ver'), parent)
+ self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
+
+ self.download_cover_button.clicked.connect(self.download_cover)
+ self.generate_cover_button.clicked.connect(self.generate_cover)
+
+ self.buttons = [self.select_cover_button, self.remove_cover_button,
+ self.trim_cover_button, self.download_cover_button,
+ self.generate_cover_button]
+
+ def select_cover(self, *args):
+ files = choose_images(self, 'change cover dialog',
+ _('Choose cover for ') +
+ self.dialog.title.current_val)
+ if not files:
+ return
+ _file = files[0]
+ if _file:
+ _file = os.path.abspath(_file)
+ if not os.access(_file, os.R_OK):
+ d = error_dialog(self, _('Cannot read'),
+ _('You do not have permission to read the file: ') + _file)
+ d.exec_()
+ return
+ cf, cover = None, None
+ try:
+ cf = open(_file, "rb")
+ cover = cf.read()
+ except IOError, e:
+ d = error_dialog(self, _('Error reading file'),
+ _("
There was an error reading from file:
")
+ + _file + "
"+str(e))
+ d.exec_()
+ if cover:
+ orig = self.current_val
+ self.current_val = cover
+ if self.current_val is None:
+ self.current_val = orig
+ error_dialog(self,
+ _("Not a valid picture"),
+ _file + _(" is not a valid picture"), show=True)
+
+ def remove_cover(self, *args):
+ self.current_val = None
+
+ def trim_cover(self, *args):
+ from calibre.utils.magick import Image
+ cdata = self.current_val
+ if not cdata:
+ return
+ im = Image()
+ im.load(cdata)
+ im.trim(10)
+ cdata = im.export('png')
+ self.current_val = cdata
+
+ def download_cover(self, *args):
+ pass # TODO: Implement this
+
+ def generate_cover(self, *args):
+ from calibre.ebooks import calibre_cover
+ from calibre.ebooks.metadata import fmt_sidx
+ from calibre.gui2 import config
+ title = self.dialog.title.current_val
+ author = authors_to_string(self.dialog.authors.current_val)
+ if not title or not author:
+ return error_dialog(self, _('Specify title and author'),
+ _('You must specify a title and author before generating '
+ 'a cover'), show=True)
+ series = self.dialog.series.current_val
+ series_string = None
+ if series:
+ series_string = _('Book %s of %s')%(
+ fmt_sidx(self.dialog.series_index.current_val,
+ use_roman=config['use_roman_numerals_for_series_number']), series)
+ self.current_val = calibre_cover(title, author,
+ series_string=series_string)
+
+ def set_pixmap_from_data(self, data):
+ if not data:
+ self.current_val = None
+ return
+ orig = self.current_val
+ self.current_val = data
+ if self.current_val is None:
+ error_dialog(self, _('Invalid cover'),
+ _('Could not change cover as the image is invalid.'),
+ show=True)
+ self.current_val = orig
+
+ def initialize(self, db, id_):
+ self._cdata = None
+ self.current_val = db.cover(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ @property
+ def changed(self):
+ return self.current_val != self.original_val
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self._cdata
+ def fset(self, cdata):
+ self._cdata = None
+ pm = QPixmap()
+ if cdata:
+ pm.loadFromData(cdata)
+ if pm.isNull():
+ pm = QPixmap(I('default_cover.png'))
+ else:
+ self._cdata = cdata
+ self.setPixmap(pm)
+ tt = _('This book has no cover')
+ if self._cdata:
+ tt = _('Cover size: %dx%d pixels') % \
+ (pm.width(), pm.height())
+ self.setToolTip(tt)
+
+ return property(fget=fget, fset=fset)
+
+ def commit(self, db, id_):
+ if self.changed:
+ if self.current_val:
+ db.set_cover(id_, self.current_val, notify=False, commit=False)
+ else:
+ db.remove_cover(id_, notify=False, commit=False)
+ return True
+
+# }}}
+
+class CommentsEdit(Editor): # {{{
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.html
+ def fset(self, val):
+ if not val or not val.strip():
+ val = ''
+ else:
+ val = comments_to_html(val)
+ self.html = val
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.current_val = db.comments(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_comment(id_, self.current_val, notify=False, commit=False)
+ return True
+# }}}
+
+class RatingEdit(QSpinBox): # {{{
+ LABEL = _('&Rating:')
+ TOOLTIP = _('Rating of this book. 0-5 stars')
+
+ def __init__(self, parent):
+ QSpinBox.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ self.setMaximum(5)
+ self.setSuffix(' ' + _('stars'))
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.value()
+ def fset(self, val):
+ if val is None:
+ val = 0
+ val = int(val)
+ if val < 0:
+ val = 0
+ if val > 5:
+ val = 5
+ self.setValue(val)
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ val = db.rating(id_, index_is_id=True)
+ if val > 0:
+ val = int(val/2.)
+ else:
+ val = 0
+ self.current_val = val
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
+class TagsEdit(CompleteLineEdit): # {{{
+ LABEL = _('Ta&gs:')
+ TOOLTIP = ''+_('Tags categorize the book. This is particularly '
+ 'useful while searching.
They can be any words'
+ 'or phrases, separated by commas.')
+
+ def __init__(self, parent):
+ CompleteLineEdit.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return [x.strip() for x in unicode(self.text()).split(',')]
+ def fset(self, val):
+ if not val:
+ val = []
+ self.setText(', '.join([x.strip() for x in val]))
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ tags = db.tags(id_, index_is_id=True)
+ tags = tags.split(',') if tags else []
+ self.current_val = tags
+ self.update_items_cache(db.all_tags())
+ self.original_val = self.current_val
+
+ @property
+ def changed(self):
+ return self.current_val != self.original_val
+
+ def edit(self, db, id_):
+ if self.changed:
+ if question_dialog(self, _('Tags changed'),
+ _('You have changed the tags. In order to use the tags'
+ ' editor, you must either discard or apply these '
+ 'changes'), show_copy_button=False,
+ buttons=QMessageBox.Apply|QMessageBox.Discard,
+ yes_button=QMessageBox.Apply):
+ self.commit(db, id_)
+ db.commit()
+ self.original_val = self.current_val
+ else:
+ self.current_val = self.original_val
+ d = TagEditor(self, db, id_)
+ if d.exec_() == TagEditor.Accepted:
+ self.current_val = d.tags
+ self.update_items_cache(db.all_tags())
+
+
+ def commit(self, db, id_):
+ db.set_tags(id_, self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
+class ISBNEdit(QLineEdit): # {{{
+ LABEL = _('IS&BN:')
+
+ def __init__(self, parent):
+ QLineEdit.__init__(self, parent)
+ self.pat = re.compile(r'[^0-9a-zA-Z]')
+ self.textChanged.connect(self.validate)
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return self.pat.sub('', unicode(self.text()).strip())
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setText(val.strip())
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.current_val = db.isbn(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ db.set_isbn(id_, self.current_val, notify=False, commit=False)
+ return True
+
+ def validate(self, *args):
+ isbn = self.current_val
+ tt = _('This ISBN number is valid')
+ if not isbn:
+ col = 'rgba(0,255,0,0%)'
+ elif check_isbn(isbn) is not None:
+ col = 'rgba(0,255,0,20%)'
+ else:
+ col = 'rgba(255,0,0,20%)'
+ tt = _('This ISBN number is invalid')
+ self.setToolTip(tt)
+ self.setStyleSheet('QLineEdit { background-color: %s }'%col)
+
+# }}}
+
+class PublisherEdit(EnComboBox): # {{{
+ LABEL = _('&Publisher:')
+
+ def __init__(self, parent):
+ EnComboBox.__init__(self, parent)
+ self.setSizeAdjustPolicy(
+ self.AdjustToMinimumContentsLengthWithIcon)
+
+ @dynamic_property
+ def current_val(self):
+
+ def fget(self):
+ return unicode(self.currentText()).strip()
+
+ def fset(self, val):
+ if not val:
+ val = ''
+ self.setEditText(val.strip())
+ self.setCursorPosition(0)
+
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ all_publishers = db.all_publishers()
+ all_publishers.sort(key=lambda x : sort_key(x[1]))
+ publisher_id = db.publisher_id(id_, index_is_id=True)
+ idx, c = None, 0
+ for i in all_publishers:
+ id, name = i
+ if id == publisher_id:
+ idx = c
+ self.addItem(name)
+ c += 1
+
+ self.setEditText('')
+ if idx is not None:
+ self.setCurrentIndex(idx)
+
+ def commit(self, db, id_):
+ db.set_publisher(id_, self.current_val, notify=False, commit=False)
+ return True
+
+# }}}
+
+
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index de59e8075d..2256816091 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -5,924 +5,17 @@
__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import textwrap, re, os
-from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \
- QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
- QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \
- QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \
- QMessageBox, QLineEdit
+from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \
+ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \
+ QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox
-from calibre.gui2 import ResizableDialog, file_icon_provider, \
- choose_files, error_dialog, choose_images, question_dialog
-from calibre.utils.icu import sort_key
-from calibre.utils.config import tweaks
-from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
- EnComboBox, FormatList, ImageView, CompleteLineEdit
-from calibre.ebooks.metadata import title_sort, authors_to_string, \
- string_to_authors, check_isbn
-from calibre.utils.date import local_tz
-from calibre import strftime
-from calibre.ebooks import BOOK_EXTENSIONS
-from calibre.customize.ui import run_plugins_on_import
-from calibre.utils.date import utcfromtimestamp
-from calibre.gui2.comments_editor import Editor
-from calibre.library.comments import comments_to_html
-from calibre.gui2.dialogs.tag_editor import TagEditor
-
-'''
-The interface common to all widgets used to set basic metadata
-class BasicMetadataWidget(object):
-
- LABEL = "label text"
-
- def initialize(self, db, id_):
- pass
-
- def commit(self, db, id_):
- return True
-
- @dynamic_property
- def current_val(self):
- # Present in most but not all basic metadata widgets
- def fget(self):
- return None
- def fset(self, val):
- pass
- return property(fget=fget, fset=fset)
-'''
-
-# Title {{{
-class TitleEdit(EnLineEdit):
-
- TITLE_ATTR = 'title'
- COMMIT = True
- TOOLTIP = _('Change the title of this book')
- LABEL = _('&Title:')
-
- def __init__(self, parent):
- self.dialog = parent
- EnLineEdit.__init__(self, parent)
- self.setToolTip(self.TOOLTIP)
- self.setWhatsThis(self.TOOLTIP)
-
- def get_default(self):
- return _('Unknown')
-
- def initialize(self, db, id_):
- title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True)
- self.current_val = title
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- title = self.current_val
- if self.COMMIT:
- getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
- else:
- getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
- commit=False)
- return True
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- title = unicode(self.text()).strip()
- if not title:
- title = self.get_default()
- return title
-
- def fset(self, val):
- if hasattr(val, 'strip'):
- val = val.strip()
- if not val:
- val = self.get_default()
- self.setText(val)
- self.setCursorPosition(0)
-
- return property(fget=fget, fset=fset)
-
-class TitleSortEdit(TitleEdit):
-
- TITLE_ATTR = 'title_sort'
- COMMIT = False
- TOOLTIP = _('Specify how this book should be sorted when by title.'
- ' For example, The Exorcist might be sorted as Exorcist, The.')
- LABEL = _('Title &sort:')
-
- def __init__(self, parent, title_edit, autogen_button):
- TitleEdit.__init__(self, parent)
- self.title_edit = title_edit
-
- base = self.TOOLTIP
- ok_tooltip = '' + textwrap.fill(base+'
'+
- _(' The green color indicates that the current '
- 'title sort matches the current title'))
- bad_tooltip = '
'+textwrap.fill(base + '
'+
- _(' The red color warns that the current '
- 'title sort does not match the current title. '
- 'No action is required if this is what you want.'))
- self.tooltips = (ok_tooltip, bad_tooltip)
-
- self.title_edit.textChanged.connect(self.update_state)
- self.textChanged.connect(self.update_state)
-
- autogen_button.clicked.connect(self.auto_generate)
- self.update_state()
-
- def update_state(self, *args):
- ts = title_sort(self.title_edit.current_val)
- normal = ts == self.current_val
- if normal:
- col = 'rgb(0, 255, 0, 20%)'
- else:
- col = 'rgb(255, 0, 0, 20%)'
- self.setStyleSheet('QLineEdit { color: black; '
- 'background-color: %s; }'%col)
- tt = self.tooltips[0 if normal else 1]
- self.setToolTip(tt)
- self.setWhatsThis(tt)
-
- def auto_generate(self, *args):
- self.current_val = title_sort(self.title_edit.current_val)
-
-# }}}
-
-# Authors {{{
-class AuthorsEdit(CompleteComboBox):
-
- TOOLTIP = ''
- LABEL = _('&Author(s):')
-
- def __init__(self, parent):
- self.dialog = parent
- CompleteComboBox.__init__(self, parent)
- self.setToolTip(self.TOOLTIP)
- self.setWhatsThis(self.TOOLTIP)
- self.setEditable(True)
- self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
-
- def get_default(self):
- return _('Unknown')
-
- def initialize(self, db, id_):
- all_authors = db.all_authors()
- all_authors.sort(key=lambda x : sort_key(x[1]))
- for i in all_authors:
- id, name = i
- name = [name.strip().replace('|', ',') for n in name.split(',')]
- self.addItem(authors_to_string(name))
-
- self.set_separator('&')
- self.set_space_before_sep(True)
- self.update_items_cache(db.all_author_names())
-
- au = db.authors(id_, index_is_id=True)
- if not au:
- au = _('Unknown')
- self.current_val = [a.strip().replace('|', ',') for a in au.split(',')]
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- authors = self.current_val
- db.set_authors(id_, authors, notify=False)
- return True
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- au = unicode(self.text()).strip()
- if not au:
- au = self.get_default()
- return string_to_authors(au)
-
- def fset(self, val):
- if not val:
- val = [self.get_default()]
- self.setEditText(' & '.join([x.strip() for x in val]))
- self.lineEdit().setCursorPosition(0)
-
-
- return property(fget=fget, fset=fset)
-
-class AuthorSortEdit(EnLineEdit):
-
- TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
- 'For example Charles Dickens should be sorted as Dickens, '
- 'Charles.\nIf the box is colored green, then text matches '
- 'the individual author\'s sort strings. If it is colored '
- 'red, then the authors and this text do not match.')
- LABEL = _('Author s&ort:')
-
- def __init__(self, parent, authors_edit, autogen_button, db):
- EnLineEdit.__init__(self, parent)
- self.authors_edit = authors_edit
- self.db = db
-
- base = self.TOOLTIP
- ok_tooltip = '
' + textwrap.fill(base+'
'+
- _(' The green color indicates that the current '
- 'author sort matches the current author'))
- bad_tooltip = '
'+textwrap.fill(base + '
'+
- _(' The red color indicates that the current '
- 'author sort does not match the current author. '
- 'No action is required if this is what you want.'))
- self.tooltips = (ok_tooltip, bad_tooltip)
-
- self.authors_edit.editTextChanged.connect(self.update_state)
- self.textChanged.connect(self.update_state)
-
- autogen_button.clicked.connect(self.auto_generate)
- self.update_state()
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- return unicode(self.text()).strip()
-
- def fset(self, val):
- if not val:
- val = ''
- self.setText(val.strip())
- self.setCursorPosition(0)
-
- return property(fget=fget, fset=fset)
-
- def update_state(self, *args):
- au = unicode(self.authors_edit.text())
- au = re.sub(r'\s+et al\.$', '', au)
- au = self.db.author_sort_from_authors(string_to_authors(au))
-
- normal = au == self.current_val
- if normal:
- col = 'rgb(0, 255, 0, 20%)'
- else:
- col = 'rgb(255, 0, 0, 20%)'
- self.setStyleSheet('QLineEdit { color: black; '
- 'background-color: %s; }'%col)
- tt = self.tooltips[0 if normal else 1]
- self.setToolTip(tt)
- self.setWhatsThis(tt)
-
- def auto_generate(self, *args):
- au = unicode(self.authors_edit.text())
- au = re.sub(r'\s+et al\.$', '', au)
- authors = string_to_authors(au)
- self.current_val = self.db.author_sort_from_authors(authors)
-
- def initialize(self, db, id_):
- self.current_val = db.author_sort(id_, index_is_id=True)
-
- def commit(self, db, id_):
- aus = self.current_val
- db.set_author_sort(id_, aus, notify=False, commit=False)
- return True
-
-# }}}
-
-# Series {{{
-class SeriesEdit(EnComboBox):
-
- TOOLTIP = _('List of known series. You can add new series.')
- LABEL = _('&Series:')
-
- def __init__(self, parent):
- EnComboBox.__init__(self, parent)
- self.dialog = parent
- self.setSizeAdjustPolicy(
- self.AdjustToMinimumContentsLengthWithIcon)
- self.setToolTip(self.TOOLTIP)
- self.setWhatsThis(self.TOOLTIP)
- self.setEditable(True)
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- return unicode(self.currentText()).strip()
-
- def fset(self, val):
- if not val:
- val = ''
- self.setEditText(val.strip())
- self.setCursorPosition(0)
-
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- all_series = db.all_series()
- all_series.sort(key=lambda x : sort_key(x[1]))
- series_id = db.series_id(id_, index_is_id=True)
- idx, c = None, 0
- for i in all_series:
- id, name = i
- if id == series_id:
- idx = c
- self.addItem(name)
- c += 1
-
- self.lineEdit().setText('')
- if idx is not None:
- self.setCurrentIndex(idx)
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- series = self.current_val
- db.set_series(id_, series, notify=False, commit=True)
- return True
-
-class SeriesIndexEdit(QDoubleSpinBox):
-
- TOOLTIP = ''
- LABEL = _('&Number:')
-
- def __init__(self, parent, series_edit):
- QDoubleSpinBox.__init__(self, parent)
- self.dialog = parent
- self.db = self.original_series_name = None
- self.setMaximum(1000000)
- self.series_edit = series_edit
- series_edit.currentIndexChanged.connect(self.enable)
- series_edit.editTextChanged.connect(self.enable)
- series_edit.lineEdit().editingFinished.connect(self.increment)
- self.enable()
-
- def enable(self, *args):
- self.setEnabled(bool(self.series_edit.current_val))
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- return self.value()
-
- def fset(self, val):
- if val is None:
- val = 1.0
- val = float(val)
- self.setValue(val)
-
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- self.db = db
- if self.series_edit.current_val:
- val = db.series_index(id_, index_is_id=True)
- else:
- val = 1.0
- self.current_val = val
- self.original_val = self.current_val
- self.original_series_name = self.series_edit.original_val
-
- def commit(self, db, id_):
- db.set_series_index(id_, self.current_val, notify=False, commit=False)
- return True
-
- def increment(self):
- if self.db is not None:
- try:
- series = self.series_edit.current_val
- if series and series != self.original_series_name:
- ns = 1.0
- if tweaks['series_index_auto_increment'] != 'const':
- ns = self.db.get_next_series_num_for(series)
- self.current_val = ns
- self.original_series_name = series
- except:
- import traceback
- traceback.print_exc()
-
-
-# }}}
-
-class BuddyLabel(QLabel): # {{{
-
- def __init__(self, buddy):
- QLabel.__init__(self, buddy.LABEL)
- self.setBuddy(buddy)
- self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
-# }}}
-
-class Format(QListWidgetItem): # {{{
-
- def __init__(self, parent, ext, size, path=None, timestamp=None):
- self.path = path
- self.ext = ext
- self.size = float(size)/(1024*1024)
- text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
- QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
- text, parent, QListWidgetItem.UserType)
- if timestamp is not None:
- ts = timestamp.astimezone(local_tz)
- t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
- text = _('Last modified: %s')%t
- self.setToolTip(text)
- self.setStatusTip(text)
-
-# }}}
-
-class FormatsManager(QWidget): # {{{
-
- def __init__(self, parent):
- QWidget.__init__(self, parent)
- self.dialog = parent
- self.changed = False
-
- self.l = l = QGridLayout()
- self.setLayout(l)
- self.cover_from_format_button = QToolButton(self)
- self.cover_from_format_button.setToolTip(
- _('Set the cover for the book from the selected format'))
- self.cover_from_format_button.setIcon(QIcon(I('book.png')))
- self.cover_from_format_button.setIconSize(QSize(32, 32))
-
- self.metadata_from_format_button = QToolButton(self)
- self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
- self.metadata_from_format_button.setIconSize(QSize(32, 32))
- # TODO: Implement the *_from_format buttons
-
- self.add_format_button = QToolButton(self)
- self.add_format_button.setIcon(QIcon(I('add_book.png')))
- self.add_format_button.setIconSize(QSize(32, 32))
- self.add_format_button.clicked.connect(self.add_format)
-
- self.remove_format_button = QToolButton(self)
- self.remove_format_button.setIcon(QIcon(I('trash.png')))
- self.remove_format_button.setIconSize(QSize(32, 32))
- self.remove_format_button.clicked.connect(self.remove_format)
-
- self.formats = FormatList(self)
- self.formats.setAcceptDrops(True)
- self.formats.formats_dropped.connect(self.formats_dropped)
- self.formats.delete_format.connect(self.remove_format)
- self.formats.itemDoubleClicked.connect(self.show_format)
- self.formats.setDragDropMode(self.formats.DropOnly)
- self.formats.setIconSize(QSize(32, 32))
- self.formats.setMaximumWidth(200)
-
- l.addWidget(self.cover_from_format_button, 0, 0, 1, 1)
- l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1)
- l.addWidget(self.add_format_button, 0, 2, 1, 1)
- l.addWidget(self.remove_format_button, 2, 2, 1, 1)
- l.addWidget(self.formats, 0, 1, 3, 1)
-
-
-
- def initialize(self, db, id_):
- self.changed = False
- exts = db.formats(id_, index_is_id=True)
- if exts:
- exts = exts.split(',')
- for ext in exts:
- if not ext:
- ext = ''
- size = db.sizeof_format(id_, ext, index_is_id=True)
- timestamp = db.format_last_modified(id_, ext)
- if size is None:
- continue
- Format(self.formats, ext, size, timestamp=timestamp)
-
- def commit(self, db, id_):
- if not self.changed:
- return True
- old_extensions, new_extensions, paths = set(), set(), {}
- for row in range(self.formats.count()):
- fmt = self.formats.item(row)
- ext, path = fmt.ext.lower(), fmt.path
- if 'unknown' in ext.lower():
- ext = None
- if path:
- new_extensions.add(ext)
- paths[ext] = path
- else:
- old_extensions.add(ext)
- for ext in new_extensions:
- db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False,
- index_is_id=True)
- db_extensions = set([f.lower() for f in db.formats(id_,
- index_is_id=True).split(',')])
- extensions = new_extensions.union(old_extensions)
- for ext in db_extensions:
- if ext not in extensions:
- db.remove_format(id_, ext, notify=False, index_is_id=True)
-
- self.changed = False
- return True
-
- def add_format(self, *args):
- files = choose_files(self, 'add formats dialog',
- _("Choose formats for ") +
- self.dialog.title.current_val,
- [(_('Books'), BOOK_EXTENSIONS)])
- self._add_formats(files)
-
- def _add_formats(self, paths):
- added = False
- if not paths:
- return added
- bad_perms = []
- for _file in paths:
- _file = os.path.abspath(_file)
- if not os.access(_file, os.R_OK):
- bad_perms.append(_file)
- continue
-
- nfile = run_plugins_on_import(_file)
- if nfile is not None:
- _file = nfile
- stat = os.stat(_file)
- size = stat.st_size
- ext = os.path.splitext(_file)[1].lower().replace('.', '')
- timestamp = utcfromtimestamp(stat.st_mtime)
- for row in range(self.formats.count()):
- fmt = self.formats.item(row)
- if fmt.ext.lower() == ext:
- self.formats.takeItem(row)
- break
- Format(self.formats, ext, size, path=_file, timestamp=timestamp)
- self.changed = True
- added = True
- if bad_perms:
- error_dialog(self, _('No permission'),
- _('You do not have '
- 'permission to read the following files:'),
- det_msg='\n'.join(bad_perms), show=True)
-
- return added
-
- def formats_dropped(self, event, paths):
- if self._add_formats(paths):
- event.accept()
-
- def remove_format(self, *args):
- rows = self.formats.selectionModel().selectedRows(0)
- for row in rows:
- self.formats.takeItem(row.row())
- self.changed = True
-
- def show_format(self, item, *args):
- fmt = item.ext
- self.dialog.view_format.emit(fmt)
-
-# }}}
-
-class Cover(ImageView): # {{{
-
- def __init__(self, parent):
- ImageView.__init__(self, parent)
- self.dialog = parent
- self._cdata = None
- self.cover_changed.connect(self.set_pixmap_from_data)
-
- self.select_cover_button = QPushButton(QIcon(I('document_open.png')),
- _('&Browse'), parent)
- self.trim_cover_button = QPushButton(QIcon(I('trim.png')),
- _('T&rim'), parent)
- self.remove_cover_button = QPushButton(QIcon(I('trash.png')),
- _('&Remove'), parent)
-
- self.select_cover_button.clicked.connect(self.select_cover)
- self.remove_cover_button.clicked.connect(self.remove_cover)
- self.trim_cover_button.clicked.connect(self.trim_cover)
-
- self.download_cover_button = QPushButton(_('Download co&ver'), parent)
- self.generate_cover_button = QPushButton(_('&Generate cover'), parent)
-
- self.download_cover_button.clicked.connect(self.download_cover)
- self.generate_cover_button.clicked.connect(self.generate_cover)
-
- self.buttons = [self.select_cover_button, self.remove_cover_button,
- self.trim_cover_button, self.download_cover_button,
- self.generate_cover_button]
-
- def select_cover(self, *args):
- files = choose_images(self, 'change cover dialog',
- _('Choose cover for ') +
- self.dialog.title.current_val)
- if not files:
- return
- _file = files[0]
- if _file:
- _file = os.path.abspath(_file)
- if not os.access(_file, os.R_OK):
- d = error_dialog(self, _('Cannot read'),
- _('You do not have permission to read the file: ') + _file)
- d.exec_()
- return
- cf, cover = None, None
- try:
- cf = open(_file, "rb")
- cover = cf.read()
- except IOError, e:
- d = error_dialog(self, _('Error reading file'),
- _("
There was an error reading from file:
")
- + _file + "
"+str(e))
- d.exec_()
- if cover:
- orig = self.current_val
- self.current_val = cover
- if self.current_val is None:
- self.current_val = orig
- error_dialog(self,
- _("Not a valid picture"),
- _file + _(" is not a valid picture"), show=True)
-
- def remove_cover(self, *args):
- self.current_val = None
-
- def trim_cover(self, *args):
- from calibre.utils.magick import Image
- cdata = self.current_val
- if not cdata:
- return
- im = Image()
- im.load(cdata)
- im.trim(10)
- cdata = im.export('png')
- self.current_val = cdata
-
- def download_cover(self, *args):
- pass # TODO: Implement this
-
- def generate_cover(self, *args):
- from calibre.ebooks import calibre_cover
- from calibre.ebooks.metadata import fmt_sidx
- from calibre.gui2 import config
- title = self.dialog.title.current_val
- author = authors_to_string(self.dialog.authors.current_val)
- if not title or not author:
- return error_dialog(self, _('Specify title and author'),
- _('You must specify a title and author before generating '
- 'a cover'), show=True)
- series = self.dialog.series.current_val
- series_string = None
- if series:
- series_string = _('Book %s of %s')%(
- fmt_sidx(self.dialog.series_index.current_val,
- use_roman=config['use_roman_numerals_for_series_number']), series)
- self.current_val = calibre_cover(title, author,
- series_string=series_string)
-
- def set_pixmap_from_data(self, data):
- if not data:
- self.current_val = None
- return
- orig = self.current_val
- self.current_val = data
- if self.current_val is None:
- error_dialog(self, _('Invalid cover'),
- _('Could not change cover as the image is invalid.'),
- show=True)
- self.current_val = orig
-
- def initialize(self, db, id_):
- self._cdata = None
- self.current_val = db.cover(id_, index_is_id=True)
- self.original_val = self.current_val
-
- @property
- def changed(self):
- return self.current_val != self.original_val
-
- @dynamic_property
- def current_val(self):
- def fget(self):
- return self._cdata
- def fset(self, cdata):
- self._cdata = None
- pm = QPixmap()
- if cdata:
- pm.loadFromData(cdata)
- if pm.isNull():
- pm = QPixmap(I('default_cover.png'))
- else:
- self._cdata = cdata
- self.setPixmap(pm)
- tt = _('This book has no cover')
- if self._cdata:
- tt = _('Cover size: %dx%d pixels') % \
- (pm.width(), pm.height())
- self.setToolTip(tt)
-
- return property(fget=fget, fset=fset)
-
- def commit(self, db, id_):
- if self.changed:
- if self.current_val:
- db.set_cover(id_, self.current_val, notify=False, commit=False)
- else:
- db.remove_cover(id_, notify=False, commit=False)
- return True
-
-# }}}
-
-class CommentsEdit(Editor): # {{{
-
- @dynamic_property
- def current_val(self):
- def fget(self):
- return self.html
- def fset(self, val):
- if not val or not val.strip():
- val = ''
- else:
- val = comments_to_html(val)
- self.html = val
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- self.current_val = db.comments(id_, index_is_id=True)
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- db.set_comment(id_, self.current_val, notify=False, commit=False)
- return True
-# }}}
-
-class RatingEdit(QSpinBox): # {{{
- LABEL = _('&Rating:')
- TOOLTIP = _('Rating of this book. 0-5 stars')
-
- def __init__(self, parent):
- QSpinBox.__init__(self, parent)
- self.setToolTip(self.TOOLTIP)
- self.setWhatsThis(self.TOOLTIP)
- self.setMaximum(5)
- self.setSuffix(' ' + _('stars'))
-
- @dynamic_property
- def current_val(self):
- def fget(self):
- return self.value()
- def fset(self, val):
- if val is None:
- val = 0
- val = int(val)
- if val < 0:
- val = 0
- if val > 5:
- val = 5
- self.setValue(val)
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- val = db.rating(id_, index_is_id=True)
- if val > 0:
- val = int(val/2.)
- else:
- val = 0
- self.current_val = val
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- db.set_rating(id_, 2*self.current_val, notify=False, commit=False)
- return True
-
-# }}}
-
-class TagsEdit(CompleteLineEdit): # {{{
- LABEL = _('Ta&gs:')
- TOOLTIP = ''+_('Tags categorize the book. This is particularly '
- 'useful while searching.
They can be any words'
- 'or phrases, separated by commas.')
-
- def __init__(self, parent):
- CompleteLineEdit.__init__(self, parent)
- self.setToolTip(self.TOOLTIP)
- self.setWhatsThis(self.TOOLTIP)
-
- @dynamic_property
- def current_val(self):
- def fget(self):
- return [x.strip() for x in unicode(self.text()).split(',')]
- def fset(self, val):
- if not val:
- val = []
- self.setText(', '.join([x.strip() for x in val]))
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- tags = db.tags(id_, index_is_id=True)
- tags = tags.split(',') if tags else []
- self.current_val = tags
- self.update_items_cache(db.all_tags())
- self.original_val = self.current_val
-
- @property
- def changed(self):
- return self.current_val != self.original_val
-
- def edit(self, db, id_):
- if self.changed:
- if question_dialog(self, _('Tags changed'),
- _('You have changed the tags. In order to use the tags'
- ' editor, you must either discard or apply these '
- 'changes'), show_copy_button=False,
- buttons=QMessageBox.Apply|QMessageBox.Discard,
- yes_button=QMessageBox.Apply):
- self.commit(db, id_)
- db.commit()
- self.original_val = self.current_val
- else:
- self.current_val = self.original_val
- d = TagEditor(self, db, id_)
- if d.exec_() == TagEditor.Accepted:
- self.current_val = d.tags
- self.update_items_cache(db.all_tags())
-
-
- def commit(self, db, id_):
- db.set_tags(id_, self.current_val, notify=False, commit=False)
- return True
-
-# }}}
-
-class ISBNEdit(QLineEdit): # {{{
- LABEL = _('IS&BN:')
-
- def __init__(self, parent):
- QLineEdit.__init__(self, parent)
- self.pat = re.compile(r'[^0-9a-zA-Z]')
- self.textChanged.connect(self.validate)
-
- @dynamic_property
- def current_val(self):
- def fget(self):
- return self.pat.sub('', unicode(self.text()).strip())
- def fset(self, val):
- if not val:
- val = ''
- self.setText(val.strip())
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- self.current_val = db.isbn(id_, index_is_id=True)
- self.original_val = self.current_val
-
- def commit(self, db, id_):
- db.set_isbn(id_, self.current_val, notify=False, commit=False)
- return True
-
- def validate(self, *args):
- isbn = self.current_val
- tt = _('This ISBN number is valid')
- if not isbn:
- col = 'rgba(0,255,0,0%)'
- elif check_isbn(isbn) is not None:
- col = 'rgba(0,255,0,20%)'
- else:
- col = 'rgba(255,0,0,20%)'
- tt = _('This ISBN number is invalid')
- self.setToolTip(tt)
- self.setStyleSheet('QLineEdit { background-color: %s }'%col)
-
-# }}}
-
-class PublisherEdit(EnComboBox): # {{{
- LABEL = _('&Publisher:')
-
- def __init__(self, parent):
- EnComboBox.__init__(self, parent)
- self.setSizeAdjustPolicy(
- self.AdjustToMinimumContentsLengthWithIcon)
-
- @dynamic_property
- def current_val(self):
-
- def fget(self):
- return unicode(self.currentText()).strip()
-
- def fset(self, val):
- if not val:
- val = ''
- self.setEditText(val.strip())
- self.setCursorPosition(0)
-
- return property(fget=fget, fset=fset)
-
- def initialize(self, db, id_):
- all_publishers = db.all_publishers()
- all_publishers.sort(key=lambda x : sort_key(x[1]))
- publisher_id = db.publisher_id(id_, index_is_id=True)
- idx, c = None, 0
- for i in all_publishers:
- id, name = i
- if id == publisher_id:
- idx = c
- self.addItem(name)
- c += 1
-
- self.setEditText('')
- if idx is not None:
- self.setCurrentIndex(idx)
-
- def commit(self, db, id_):
- db.set_publisher(id_, self.current_val, notify=False, commit=False)
- return True
-
-# }}}
+from calibre.ebooks.metadata import authors_to_string, string_to_authors
+from calibre.gui2 import ResizableDialog
+from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
+ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
+ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
+ BuddyLabel
class MetadataSingleDialog(ResizableDialog):
From 96be6e90351041cac47cb5f17efc4fd1cd9d672b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 16:03:54 -0700
Subject: [PATCH 42/61] Basic metadata widgets layout complete
---
src/calibre/gui2/metadata/basic_widgets.py | 67 ++++++++++++++++++++--
src/calibre/gui2/metadata/single.py | 35 +++++++++--
2 files changed, 92 insertions(+), 10 deletions(-)
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index eb162ac9d5..5d37e854da 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -7,11 +7,10 @@
import textwrap, re, os
-from PyQt4.Qt import Qt, \
+from PyQt4.Qt import Qt, QDateEdit, QDate, \
QIcon, QToolButton, QWidget, QLabel, QGridLayout, \
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \
- QPushButton, QSpinBox, \
- QMessageBox, QLineEdit
+ QPushButton, QSpinBox, QMessageBox, QLineEdit
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
EnComboBox, FormatList, ImageView, CompleteLineEdit
@@ -19,9 +18,9 @@
from calibre.utils.config import tweaks
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors, check_isbn
-from calibre.gui2 import file_icon_provider, \
+from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog
-from calibre.utils.date import local_tz
+from calibre.utils.date import local_tz, qt_to_dt
from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import
@@ -924,4 +923,62 @@ def commit(self, db, id_):
# }}}
+class DateEdit(QDateEdit): # {{{
+ TOOLTIP = ''
+ LABEL = _('&Date:')
+ FMT = 'd MMM yyyy'
+ ATTR = 'timestamp'
+
+ def __init__(self, parent):
+ QDateEdit.__init__(self, parent)
+ self.setToolTip(self.TOOLTIP)
+ self.setWhatsThis(self.TOOLTIP)
+ fmt = self.FMT
+ if fmt is None:
+ fmt = tweaks['gui_pubdate_display_format']
+ if fmt is None:
+ fmt = 'MMM yyyy'
+ self.setDisplayFormat(fmt)
+ self.setCalendarPopup(True)
+ self.setMinimumDate(UNDEFINED_QDATE)
+ self.setSpecialValueText(_('Undefined'))
+ self.clear_button = QToolButton(parent)
+ self.clear_button.setIcon(QIcon(I('trash.png')))
+ self.clear_button.setToolTip(_('Clear date'))
+ self.clear_button.clicked.connect(self.reset_date)
+
+ def reset_date(self, *args):
+ self.current_val = None
+
+ @dynamic_property
+ def current_val(self):
+ def fget(self):
+ return qt_to_dt(self.date())
+ def fset(self, val):
+ if val is None:
+ val = UNDEFINED_DATE
+ self.setDate(QDate(val.year, val.month, val.day))
+ return property(fget=fget, fset=fset)
+
+ def initialize(self, db, id_):
+ self.current_val = getattr(db, self.ATTR)(id_, index_is_id=True)
+ self.original_val = self.current_val
+
+ def commit(self, db, id_):
+ if self.changed:
+ getattr(db, 'set_'+self.ATTR)(id_, self.current_val, commit=False,
+ notify=False)
+ return True
+
+ @property
+ def changed(self):
+ o, c = self.original_val, self.current_val
+ return o.year != c.year or o.month != c.month or o.day != c.day
+
+class PubdateEdit(DateEdit):
+ LABEL = _('Publishe&d:')
+ FMT = None
+ ATTR = 'pubdate'
+
+# }}}
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 2256816091..730e5f10b6 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -6,16 +6,17 @@
__docformat__ = 'restructuredtext en'
-from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \
- QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \
- QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox
+from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
+ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
+ QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
+ QSizePolicy
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
- BuddyLabel
+ BuddyLabel, DateEdit, PubdateEdit
class MetadataSingleDialog(ResizableDialog):
@@ -119,6 +120,17 @@ def create_basic_metadata_widgets(self): # {{{
self.publisher = PublisherEdit(self)
self.basic_metadata_widgets.append(self.publisher)
+ self.timestamp = DateEdit(self)
+ self.pubdate = PubdateEdit(self)
+ self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
+
+ self.fetch_metadata_button = QPushButton(
+ _('&Fetch metadata from server'), self)
+ self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
+ font = self.fmb_font = QFont()
+ font.setBold(True)
+ self.fetch_metadata_button.setFont(font)
+
# }}}
def do_layout(self): # {{{
@@ -172,6 +184,7 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
l.setMargin(0)
self.splitter.addWidget(w)
def create_row2(row, widget, button=None):
+ row += 1
ql = BuddyLabel(widget)
l.addWidget(ql, row, 0, 1, 1)
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
@@ -179,10 +192,19 @@ def create_row2(row, widget, button=None):
l.addWidget(button, row, 2, 1, 1)
l.addWidget(gb, 0, 0, 1, 3)
+ self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
+ QSizePolicy.Expanding)
+ l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
create_row2(1, self.rating)
create_row2(2, self.tags, self.tags_editor_button)
create_row2(3, self.isbn)
- create_row2(4, self.publisher)
+ create_row2(4, self.timestamp, self.timestamp.clear_button)
+ create_row2(5, self.pubdate, self.pubdate.clear_button)
+ create_row2(6, self.publisher)
+ self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
+ QSizePolicy.Expanding)
+ l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
+ l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
gb.l = l = QVBoxLayout()
@@ -219,6 +241,9 @@ def remove_unused_series(self, *args):
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
+ def fetch_metadata(self, *args):
+ pass # TODO: fetch metadata
+
if __name__ == '__main__':
from PyQt4.Qt import QApplication
From d3bd5b07e8268d1ff3b79f15cae60b5e9efa87d4 Mon Sep 17 00:00:00 2001
From: ldolse
Date: Fri, 21 Jan 2011 08:35:14 +0800
Subject: [PATCH 43/61] false positive tuning in txt input and dehyphenate
---
src/calibre/ebooks/conversion/preprocess.py | 4 ++++
src/calibre/ebooks/txt/processor.py | 4 ++--
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py
index 5fceeb7aed..691aa307d7 100644
--- a/src/calibre/ebooks/conversion/preprocess.py
+++ b/src/calibre/ebooks/conversion/preprocess.py
@@ -224,6 +224,10 @@ def dehyphenate(self, match):
return firsthalf+u'\u2014'+wraptags+secondhalf
else:
+ if self.format == 'individual_words' and len(firsthalf) + len(secondhalf) <= 6:
+ if self.verbose > 2:
+ self.log("too short, returned hyphenated word: " + str(hyphenated))
+ return hyphenated
if len(firsthalf) <= 2 and len(secondhalf) <= 2:
if self.verbose > 2:
self.log("too short, returned hyphenated word: " + str(hyphenated))
diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py
index 9fd8af0d70..43aadc6576 100644
--- a/src/calibre/ebooks/txt/processor.py
+++ b/src/calibre/ebooks/txt/processor.py
@@ -175,9 +175,9 @@ def detect_formatting_type(txt):
# Block quote.
textile_count += len(re.findall(r'(?mu)^bq\.', txt))
# Images
- textile_count += len(re.findall(r'\![^\s]+(:[^\s]+)*', txt))
+ textile_count += len(re.findall(r'\![^\s]+(?=.*?/)(:[^\s]+)*', txt))
# Links
- textile_count += len(re.findall(r'"(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt))
+ textile_count += len(re.findall(r'"(?=".*?\()(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt))
if markdown_count > 5 or textile_count > 5:
if markdown_count > textile_count:
From a8aa6ef54aeeacab8a1f8045a0aff87db61e19ed Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 19:10:54 -0700
Subject: [PATCH 44/61] Implement accept in the new metadata dialog
---
src/calibre/gui2/metadata/single.py | 48 ++++++++++++++++++++++++++++-
1 file changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 730e5f10b6..b53637d66a 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -12,7 +12,7 @@
QSizePolicy
from calibre.ebooks.metadata import authors_to_string, string_to_authors
-from calibre.gui2 import ResizableDialog
+from calibre.gui2 import ResizableDialog, error_dialog, gprefs
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
@@ -53,12 +53,16 @@ def setupUi(self, *args): # {{{
self.create_basic_metadata_widgets()
self.do_layout()
+ geom = gprefs.get('metasingle_window_geometry3', None)
+ if geom is not None:
+ self.restoreGeometry(bytes(geom))
# }}}
def create_basic_metadata_widgets(self): # {{{
self.basic_metadata_widgets = []
self.title = TitleEdit(self)
+ self.title.textChanged.connect(self.update_window_title)
self.deduce_title_sort_button = QToolButton(self)
self.deduce_title_sort_button.setToolTip(
_('Automatically create the title sort entry based on the current '
@@ -144,6 +148,9 @@ def do_layout(self): # {{{
self.tabs[0].setLayout(l)
l.addLayout(tl)
+ sto = QWidget.setTabOrder
+ sto(self.fetch_metadata_button, self.title)
+
def create_row(row, one, two, three, col=1, icon='forward.png'):
ql = BuddyLabel(one)
tl.addWidget(ql, row, col+0, 1, 1)
@@ -156,13 +163,18 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
tl.addWidget(ql, row, col+3, 1, 1)
self.labels.append(ql)
tl.addWidget(three, row, col+4, 1, 1)
+ sto(one, two)
+ sto(two, three)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
+ sto(self.title_sort, self.authors)
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
+ sto(self.author_sort, self.series)
create_row(2, self.series, self.remove_unused_series_button,
self.series_index, icon='trash.png')
+ sto(self.series_index, self.swap_title_author_button)
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
@@ -219,6 +231,16 @@ def __call__(self, id_, has_next=False, has_previous=False):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
+ self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
+
+
+ def update_window_title(self, *args):
+ title = self.title.current_val
+ if len(title) > 50:
+ title = title[:50] + u'\u2026'
+ self.setWindowTitle(_('Edit Meta Information') + ' - ' +
+ title)
+
def swap_title_author(self, *args):
title = self.title.current_val
@@ -244,6 +266,30 @@ def tags_editor(self, *args):
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
+ def accept(self):
+ for widget in self.basic_metadata_widgets:
+ try:
+ if not widget.commit(self.db, self.book_id):
+ return
+ except IOError, err:
+ if err.errno == 13: # Permission denied
+ import traceback
+ fname = err.filename if err.filename else 'file'
+ return error_dialog(self, _('Permission denied'),
+ _('Could not open %s. Is it being used by another'
+ ' program?')%fname, det_msg=traceback.format_exc(),
+ show=True)
+ raise
+ self.save_state()
+ ResizableDialog.accept(self)
+
+ def reject(self):
+ self.save_state()
+ ResizableDialog.reject(self)
+
+ def save_state(self):
+ gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
+
if __name__ == '__main__':
from PyQt4.Qt import QApplication
From 5a2cbbc8c41dfe0e226b945c7496e2ba67b276b5 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 19:14:20 -0700
Subject: [PATCH 45/61] Fix #8483 (Need Support for Archos 101)
---
src/calibre/devices/android/driver.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 1b5cbe4bed..a95e3c46fa 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -54,7 +54,7 @@ class ANDROID(USBMS):
0x1004 : { 0x61cc : [0x100] },
# Archos
- 0x0e79 : { 0x1420 : [0x0216]},
+ 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]},
}
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
@@ -70,10 +70,10 @@ class ANDROID(USBMS):
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
- 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID']
+ 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
- 'A70S']
+ 'A70S', 'A101IT']
OSX_MAIN_MEM = 'Android Device Main Memory'
From 2e7967bd6e70670a3a57063bbc4647bb54c253f0 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 19:45:59 -0700
Subject: [PATCH 46/61] Fix #7702 (Option to populate author from selected row
when adding empty books)
---
src/calibre/gui2/actions/add.py | 22 ++++--
src/calibre/gui2/dialogs/add_empty_book.py | 85 ++++++++++++++++++++++
2 files changed, 101 insertions(+), 6 deletions(-)
create mode 100644 src/calibre/gui2/dialogs/add_empty_book.py
diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py
index 6fa53d6290..7c454d0a94 100644
--- a/src/calibre/gui2/actions/add.py
+++ b/src/calibre/gui2/actions/add.py
@@ -8,11 +8,12 @@
import os
from functools import partial
-from PyQt4.Qt import QInputDialog, QPixmap, QMenu
+from PyQt4.Qt import QPixmap, QMenu
from calibre.gui2 import error_dialog, choose_files, \
choose_dir, warning_dialog, info_dialog
+from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.filenames import ascii_filename
@@ -42,7 +43,7 @@ def genesis(self):
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addSeparator()
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
- 'formats)'), self.add_empty)
+ 'formats)'), self.add_empty, _('Shift+Ctrl+E'))
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
self.qaction.setMenu(self.add_menu)
self.qaction.triggered.connect(self.add_books)
@@ -83,12 +84,21 @@ def add_empty(self, *args):
Add an empty book item to the library. This does not import any formats
from a book file.
'''
- num, ok = QInputDialog.getInt(self.gui, _('How many empty books?'),
- _('How many empty books should be added?'), 1, 1, 100)
- if ok:
+ author = None
+ index = self.gui.library_view.currentIndex()
+ if index.isValid():
+ raw = index.model().db.authors(index.row())
+ if raw:
+ authors = [a.strip().replace('|', ',') for a in raw.split(',')]
+ if authors:
+ author = authors[0]
+ dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author)
+ if dlg.exec_() == dlg.Accepted:
+ num = dlg.qty_to_add
from calibre.ebooks.metadata import MetaInformation
for x in xrange(num):
- self.gui.library_view.model().db.import_book(MetaInformation(None), [])
+ mi = MetaInformation(_('Unknown'), dlg.selected_authors)
+ self.gui.library_view.model().db.import_book(mi, [])
self.gui.library_view.model().books_added(num)
def add_isbns(self, books, add_tags=[]):
diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py
new file mode 100644
index 0000000000..b8339f95f5
--- /dev/null
+++ b/src/calibre/gui2/dialogs/add_empty_book.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
+__docformat__ = 'restructuredtext en'
+__license__ = 'GPL v3'
+
+
+from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \
+ QApplication, QSpinBox, QToolButton, QIcon
+from calibre.ebooks.metadata import authors_to_string, string_to_authors
+from calibre.gui2.widgets import CompleteComboBox
+from calibre.utils.icu import sort_key
+
+class AddEmptyBookDialog(QDialog):
+
+ def __init__(self, parent, db, author):
+ QDialog.__init__(self, parent)
+ self.db = db
+
+ self.setWindowTitle(_('How many empty books?'))
+
+ self._layout = QGridLayout(self)
+ self.setLayout(self._layout)
+
+ self.qty_label = QLabel(_('How many empty books should be added?'))
+ self._layout.addWidget(self.qty_label, 0, 0, 1, 2)
+
+ self.qty_spinbox = QSpinBox(self)
+ self.qty_spinbox.setRange(1, 10000)
+ self.qty_spinbox.setValue(1)
+ self._layout.addWidget(self.qty_spinbox, 1, 0, 1, 2)
+
+ self.author_label = QLabel(_('Set the author of the new books to:'))
+ self._layout.addWidget(self.author_label, 2, 0, 1, 2)
+
+ self.authors_combo = CompleteComboBox(self)
+ self.authors_combo.setSizeAdjustPolicy(
+ self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
+ self.authors_combo.setEditable(True)
+ self._layout.addWidget(self.authors_combo, 3, 0, 1, 1)
+ self.initialize_authors(db, author)
+
+ self.clear_button = QToolButton(self)
+ self.clear_button.setIcon(QIcon(I('trash.png')))
+ self.clear_button.setToolTip(_('Reset author to Unknown'))
+ self.clear_button.clicked.connect(self.reset_author)
+ self._layout.addWidget(self.clear_button, 3, 1, 1, 1)
+
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ self._layout.addWidget(button_box)
+ self.resize(self.sizeHint())
+
+ def reset_author(self, *args):
+ self.authors_combo.setEditText(_('Unknown'))
+
+ def initialize_authors(self, db, author):
+ all_authors = db.all_authors()
+ all_authors.sort(key=lambda x : sort_key(x[1]))
+ for i in all_authors:
+ id, name = i
+ name = [name.strip().replace('|', ',') for n in name.split(',')]
+ self.authors_combo.addItem(authors_to_string(name))
+
+ au = author
+ if not au:
+ au = _('Unknown')
+ self.authors_combo.setEditText(au.replace('|', ','))
+
+ self.authors_combo.set_separator('&')
+ self.authors_combo.set_space_before_sep(True)
+ self.authors_combo.update_items_cache(db.all_author_names())
+
+ @property
+ def qty_to_add(self):
+ return self.qty_spinbox.value()
+
+ @property
+ def selected_authors(self):
+ return string_to_authors(unicode(self.authors_combo.text()))
+
+if __name__ == '__main__':
+ app = QApplication([])
+ d = AddEmptyBookDialog()
+ d.exec_()
From 3e7afccf7e8711a4e1f631861532ee1c31489007 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 20:28:58 -0700
Subject: [PATCH 47/61] ...
---
src/calibre/gui2/actions/choose_library.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index 6f4e883b1a..a2679c2482 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -32,7 +32,7 @@ def write_stats(self):
locs = list(self.stats.keys())
locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]),
reverse=True)
- for key in locs[15:]:
+ for key in locs[25:]:
self.stats.pop(key)
gprefs.set('library_usage_stats', self.stats)
From 96a369f1a55577b567190304cfe299c53e417ce6 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 20:31:54 -0700
Subject: [PATCH 48/61] Link up get metadata and cover from format buttons
---
src/calibre/gui2/metadata/basic_widgets.py | 34 +++++++++++-
src/calibre/gui2/metadata/single.py | 64 +++++++++++++++++++++-
2 files changed, 95 insertions(+), 3 deletions(-)
diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py
index 5d37e854da..96ea785ff2 100644
--- a/src/calibre/gui2/metadata/basic_widgets.py
+++ b/src/calibre/gui2/metadata/basic_widgets.py
@@ -15,9 +15,10 @@
from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \
EnComboBox, FormatList, ImageView, CompleteLineEdit
from calibre.utils.icu import sort_key
-from calibre.utils.config import tweaks
+from calibre.utils.config import tweaks, prefs
from calibre.ebooks.metadata import title_sort, authors_to_string, \
string_to_authors, check_isbn
+from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog
from calibre.utils.date import local_tz, qt_to_dt
@@ -440,7 +441,6 @@ def __init__(self, parent):
self.metadata_from_format_button = QToolButton(self)
self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png')))
self.metadata_from_format_button.setIconSize(QSize(32, 32))
- # TODO: Implement the *_from_format buttons
self.add_format_button = QToolButton(self)
self.add_format_button.setIcon(QIcon(I('add_book.png')))
@@ -565,6 +565,36 @@ def show_format(self, item, *args):
fmt = item.ext
self.dialog.view_format.emit(fmt)
+ def get_selected_format_metadata(self, db, id_):
+ old = prefs['read_file_metadata']
+ if not old:
+ prefs['read_file_metadata'] = True
+ try:
+ row = self.formats.currentRow()
+ fmt = self.formats.item(row)
+ if fmt is None:
+ if self.formats.count() == 1:
+ fmt = self.formats.item(0)
+ if fmt is None:
+ error_dialog(self, _('No format selected'),
+ _('No format selected')).exec_()
+ return None, None
+ ext = fmt.ext.lower()
+ if fmt.path is None:
+ stream = db.format(id_, ext, as_file=True, index_is_id=True)
+ else:
+ stream = open(fmt.path, 'r+b')
+ try:
+ mi = get_metadata(stream, ext)
+ return mi, ext
+ except:
+ error_dialog(self, _('Could not read metadata'),
+ _('Could not read metadata from %s format')%ext).exec_()
+ return None, None
+ finally:
+ if old != prefs['read_file_metadata']:
+ prefs['read_file_metadata'] = old
+
# }}}
class Cover(ImageView): # {{{
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index b53637d66a..c7b5e7f99b 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -5,6 +5,7 @@
__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+import os
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
@@ -101,7 +102,10 @@ def create_basic_metadata_widgets(self): # {{{
self.formats_manager = FormatsManager(self)
self.basic_metadata_widgets.append(self.formats_manager)
-
+ self.formats_manager.metadata_from_format_button.clicked.connect(
+ self.metadata_from_format)
+ self.formats_manager.cover_from_format_button.clicked.connect(
+ self.cover_from_format)
self.cover = Cover(self)
self.basic_metadata_widgets.append(self.cover)
@@ -263,6 +267,64 @@ def remove_unused_series(self, *args):
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
+ def metadata_from_format(self, *args):
+ mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
+ self.book_id)
+ if mi is not None:
+ self.update_from_mi(mi)
+
+ def cover_from_format(self, *args):
+ mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
+ self.book_id)
+ if mi is None:
+ return
+ cdata = None
+ if mi.cover and os.access(mi.cover, os.R_OK):
+ cdata = open(mi.cover).read()
+ elif mi.cover_data[1] is not None:
+ cdata = mi.cover_data[1]
+ if cdata is None:
+ error_dialog(self, _('Could not read cover'),
+ _('Could not read cover from %s format')%ext).exec_()
+ return
+ orig = self.cover.current_val
+ self.cover.current_val = cdata
+ if self.cover.current_val is None:
+ self.cover.current_val = orig
+ return error_dialog(self, _('Could not read cover'),
+ _('The cover in the %s format is invalid')%ext,
+ show=True)
+ return
+
+ def update_from_mi(self, mi):
+ if not mi.is_null('title'):
+ self.title.current_val = mi.title
+ if not mi.is_null('authors'):
+ self.authors.current_val = mi.authors
+ if not mi.is_null('author_sort'):
+ self.author_sort.current_val = mi.author_sort
+ if not mi.is_null('rating'):
+ try:
+ self.rating.current_val = mi.rating
+ except:
+ pass
+ if not mi.is_null('publisher'):
+ self.publisher.current_val = mi.publisher
+ if not mi.is_null('tags'):
+ self.tags.current_val = mi.tags
+ if not mi.is_null('isbn'):
+ self.isbn.current_val = mi.isbn
+ if not mi.is_null('pubdate'):
+ self.pubdate.current_val = mi.pubdate
+ if not mi.is_null('series') and mi.series.strip():
+ self.series.current_val = mi.series
+ if mi.series_index is not None:
+ self.series_index.current_val = float(mi.series_index)
+ if mi.comments and mi.comments.strip():
+ self.comments.current_val = mi.comments
+
+
+
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
From 38e6416c3b69b6f98c35b34726259683cff0c800 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 20:58:32 -0700
Subject: [PATCH 49/61] completed implementation of new metadata dialog, except
for downloading of metadata which is also going to be refactored, separately
---
src/calibre/gui2/custom_column_widgets.py | 3 +-
src/calibre/gui2/metadata/single.py | 51 ++++++++++++++++++++---
2 files changed, 48 insertions(+), 6 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 58985d1121..c873d1ed94 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -379,7 +379,8 @@ def widget_factory(type, col):
w = bulk_widgets[type](db, col, parent)
else:
w = widgets[type](db, col, parent)
- w.initialize(book_id)
+ if book_id is not None:
+ w.initialize(book_id)
return w
x = db.custom_column_num_map
cols = list(x)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index c7b5e7f99b..26acf944e1 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -18,6 +18,8 @@
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \
BuddyLabel, DateEdit, PubdateEdit
+from calibre.gui2.custom_column_widgets import populate_metadata_page
+from calibre.utils.config import tweaks
class MetadataSingleDialog(ResizableDialog):
@@ -53,6 +55,12 @@ def setupUi(self, *args): # {{{
self.create_basic_metadata_widgets()
+ if len(self.db.custom_column_label_map) == 0:
+ self.central_widget.tabBar().setVisible(False)
+ else:
+ self.create_custom_metadata_widgets()
+
+
self.do_layout()
geom = gprefs.get('metasingle_window_geometry3', None)
if geom is not None:
@@ -139,6 +147,25 @@ def create_basic_metadata_widgets(self): # {{{
font.setBold(True)
self.fetch_metadata_button.setFont(font)
+
+ # }}}
+
+ def create_custom_metadata_widgets(self): # {{{
+ self.custom_metadata_widgets_parent = w = QWidget(self)
+ layout = QGridLayout()
+ w.setLayout(layout)
+ self.custom_metadata_widgets, self.__cc_spacers = \
+ populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
+ two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
+ self.__custom_col_layouts = [layout]
+ ans = self.custom_metadata_widgets
+ for i in range(len(ans)-1):
+ if len(ans[i+1].widgets) == 2:
+ w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
+ else:
+ w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
+ for c in range(2, len(ans[i].widgets), 2):
+ w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
# }}}
def do_layout(self): # {{{
@@ -150,6 +177,10 @@ def do_layout(self): # {{{
self.tabs[0].l = l = QVBoxLayout()
self.tabs[0].tl = tl = QGridLayout()
self.tabs[0].setLayout(l)
+ w = getattr(self, 'custom_metadata_widgets_parent', None)
+ if w is not None:
+ self.tabs.append(w)
+ self.central_widget.addTab(w, _('&Custom metadata'))
l.addLayout(tl)
sto = QWidget.setTabOrder
@@ -235,6 +266,8 @@ def __call__(self, id_, has_next=False, has_previous=False):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
+ for widget in self.custom_metadata_widgets:
+ widget.initialize(id_)
self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
@@ -323,26 +356,34 @@ def update_from_mi(self, mi):
if mi.comments and mi.comments.strip():
self.comments.current_val = mi.comments
-
-
def fetch_metadata(self, *args):
pass # TODO: fetch metadata
- def accept(self):
+ def apply_changes(self):
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
- return
+ return False
except IOError, err:
if err.errno == 13: # Permission denied
import traceback
fname = err.filename if err.filename else 'file'
- return error_dialog(self, _('Permission denied'),
+ error_dialog(self, _('Permission denied'),
_('Could not open %s. Is it being used by another'
' program?')%fname, det_msg=traceback.format_exc(),
show=True)
+ return False
raise
+ for widget in getattr(self, 'custom_metadata_widgets', []):
+ widget.commit(self.book_id)
+
+ self.db.commit()
+ return True
+
+ def accept(self):
self.save_state()
+ if not self.apply_changes():
+ return
ResizableDialog.accept(self)
def reject(self):
From 6cc052260bf88895a6fbd171410c9fdd846e6059 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 22:27:12 -0700
Subject: [PATCH 50/61] Fix tab order
---
src/calibre/gui2/metadata/single.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 26acf944e1..8b0e3da2d2 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -219,11 +219,14 @@ def create_row(row, one, two, three, col=1, icon='forward.png'):
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
gb.l = l = QGridLayout()
gb.setLayout(l)
+ sto(self.swap_title_author_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
l.addWidget(b, 0, i, 1, 1)
+ sto(b, self.cover.buttons[i+1])
gb.hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
gb.hl.addWidget(b)
+ sto(self.cover.buttons[-2], self.cover.buttons[-1])
l.addLayout(gb.hl, 1, 0, 1, 3)
self.tabs[0].middle = w = QWidget(self)
w.l = l = QGridLayout()
@@ -237,23 +240,31 @@ def create_row2(row, widget, button=None):
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
if button is not None:
l.addWidget(button, row, 2, 1, 1)
+ if button is not None:
+ sto(widget, button)
l.addWidget(gb, 0, 0, 1, 3)
self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
+ sto(self.cover.buttons[-1], self.rating)
create_row2(1, self.rating)
+ sto(self.rating, self.tags)
create_row2(2, self.tags, self.tags_editor_button)
+ sto(self.tags_editor_button, self.isbn)
create_row2(3, self.isbn)
+ sto(self.isbn, self.timestamp)
create_row2(4, self.timestamp, self.timestamp.clear_button)
+ sto(self.timestamp.clear_button, self.pubdate)
create_row2(5, self.pubdate, self.pubdate.clear_button)
+ sto(self.pubdate.clear_button, self.publisher)
create_row2(6, self.publisher)
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
- self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
+ self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self)
gb.l = l = QVBoxLayout()
gb.setLayout(l)
l.addWidget(self.comments)
From 5203777b2ab72372e33d16070c434d5074a3b562 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 23:09:22 -0700
Subject: [PATCH 51/61] next, previous buttons
---
src/calibre/gui2/metadata/single.py | 70 ++++++++++++++++++++++++++---
1 file changed, 63 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 8b0e3da2d2..99d10a156d 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -6,6 +6,7 @@
__docformat__ = 'restructuredtext en'
import os
+from functools import partial
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
@@ -27,6 +28,7 @@ class MetadataSingleDialog(ResizableDialog):
def __init__(self, db, parent=None):
self.db = db
+ self.changed = set([])
ResizableDialog.__init__(self, parent)
def setupUi(self, *args): # {{{
@@ -37,6 +39,14 @@ def setupUi(self, *args): # {{{
self)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
+ self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
+ self)
+ self.next_button.clicked.connect(partial(self.do_one, delta=1))
+ self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
+ self)
+ self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
+ self.button_box.addButton(self.next_button, self.button_box.ActionRole)
+ self.prev_button.clicked.connect(partial(self.do_one, delta=-1))
self.scroll_area = QScrollArea(self)
self.scroll_area.setFrameShape(QScrollArea.NoFrame)
@@ -184,6 +194,7 @@ def do_layout(self): # {{{
l.addLayout(tl)
sto = QWidget.setTabOrder
+ sto(self.button_box, self.fetch_metadata_button)
sto(self.fetch_metadata_button, self.title)
def create_row(row, one, two, three, col=1, icon='forward.png'):
@@ -272,14 +283,14 @@ def create_row2(row, widget, button=None):
# }}}
- def __call__(self, id_, has_next=False, has_previous=False):
- # TODO: Next and previous buttons
+ def __call__(self, id_):
self.book_id = id_
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
for widget in self.custom_metadata_widgets:
widget.initialize(id_)
- self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
+ # Commented out as it doesn't play nice with Next, Prev buttons
+ #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
def update_window_title(self, *args):
@@ -289,7 +300,6 @@ def update_window_title(self, *args):
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
title)
-
def swap_title_author(self, *args):
title = self.title.current_val
self.title.current_val = authors_to_string(self.authors.current_val)
@@ -371,6 +381,7 @@ def fetch_metadata(self, *args):
pass # TODO: fetch metadata
def apply_changes(self):
+ self.changed.add(self.book_id)
for widget in self.basic_metadata_widgets:
try:
if not widget.commit(self.db, self.book_id):
@@ -404,13 +415,58 @@ def reject(self):
def save_state(self):
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
+ def start(self, row_list, current_row, view_slot=None):
+ self.row_list = row_list
+ self.current_row = current_row
+ if view_slot is not None:
+ self.view_format.connect(view_slot)
+ self.do_one()
+ ret = self.exec_()
+ self.break_cycles()
+ return ret
+
+ def do_one(self, delta=0):
+ self.current_row += delta
+ prev = next_ = None
+ if self.current_row > 0:
+ prev = self.db.title(self.row_list[self.current_row-1])
+ if self.current_row < len(self.row_list) - 1:
+ next_ = self.db.title(self.row_list[self.current_row+1])
+
+ if next_ is not None:
+ tip = _('Save changes and edit the metadata of %s')%next_
+ self.next_button.setToolTip(tip)
+ self.next_button.setVisible(next_ is not None)
+ if prev is not None:
+ tip = _('Save changes and edit the metadata of %s')%prev
+ self.prev_button.setToolTip(tip)
+ self.prev_button.setVisible(prev is not None)
+ self(self.db.id(self.row_list[self.current_row]))
+
+ def break_cycles(self):
+ # Break any reference cycles that could prevent python
+ # from garbage collecting this dialog
+ def disconnect(signal):
+ try:
+ signal.disconnect()
+ except:
+ pass # Fails if view format was never connected
+ disconnect(self.view_format)
+ for b in ('next_button', 'prev_button'):
+ x = getattr(self, b, None)
+ if x is not None:
+ disconnect(x.clicked)
+
+def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
+ d = MetadataSingleDialog(db, parent)
+ d.start(row_list, current_row, view_slot=view_slot)
+ return d.changed
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
from calibre.library import db
db = db()
- d = MetadataSingleDialog(db)
- d(db.data[0][0])
- d.exec_()
+ row_list = list(range(len(db.data)))
+ edit_metadata(db, row_list, 0)
From ca3f2dafc52223d79a1cc64422965a10c86f9db2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 23:23:12 -0700
Subject: [PATCH 52/61] ...
---
src/calibre/gui2/metadata/single.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 99d10a156d..32fa6ea4f3 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -192,6 +192,8 @@ def do_layout(self): # {{{
self.tabs.append(w)
self.central_widget.addTab(w, _('&Custom metadata'))
l.addLayout(tl)
+ l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding,
+ QSizePolicy.Fixed))
sto = QWidget.setTabOrder
sto(self.button_box, self.fetch_metadata_button)
From 229fe3e4d2c32830367d8ec5b2dc66bd567f0900 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 20 Jan 2011 23:58:22 -0700
Subject: [PATCH 53/61] Add a confirmation when closing the add a custom news
source dialog. Fixes #8460 (Add custom new source interface)
---
src/calibre/gui2/dialogs/user_profiles.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/calibre/gui2/dialogs/user_profiles.py b/src/calibre/gui2/dialogs/user_profiles.py
index 04c41f0c5e..fe64deb430 100644
--- a/src/calibre/gui2/dialogs/user_profiles.py
+++ b/src/calibre/gui2/dialogs/user_profiles.py
@@ -356,6 +356,13 @@ def clear(self):
self.populate_options(AutomaticNewsRecipe)
self.source_code.setText('')
+ def reject(self):
+ if question_dialog(self, _('Are you sure?'),
+ _('You will lose any unsaved changes. To save your'
+ ' changes, click the Add/Update recipe button.'
+ ' Continue?'), show_copy_button=False):
+ ResizableDialog.reject(self)
+
if __name__ == '__main__':
from calibre.gui2 import is_ok_to_use_qt
is_ok_to_use_qt()
From 7f2373c6bea1702447631ddcce12197dc2679da9 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 21 Jan 2011 00:06:47 -0700
Subject: [PATCH 54/61] ...
---
src/calibre/trac/bzr_commit_plugin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/trac/bzr_commit_plugin.py b/src/calibre/trac/bzr_commit_plugin.py
index 6c36115cae..2f91804315 100644
--- a/src/calibre/trac/bzr_commit_plugin.py
+++ b/src/calibre/trac/bzr_commit_plugin.py
@@ -104,12 +104,12 @@ def run(self, message=None, file=None, verbose=False, selected_list=None,
def close_bug(self, bug, action, url, config):
print 'Closing bug #%s'% bug
- nick = config.get_nickname()
+ #nick = config.get_nickname()
suffix = config.get_user_option('bug_close_comment')
if suffix is None:
suffix = 'The fix will be in the next release.'
action = action+'ed'
- msg = '%s in branch %s. %s'%(action, nick, suffix)
+ msg = '%s in branch %s. %s'%(action, 'lp:calibre', suffix)
msg = msg.replace('Fixesed', 'Fixed')
server = xmlrpclib.ServerProxy(url)
server.ticket.update(int(bug), msg,
From 29e75fb5d192eab7b6067483099880da17bf8b75 Mon Sep 17 00:00:00 2001
From: GRiker
Date: Fri, 21 Jan 2011 04:09:57 -0700
Subject: [PATCH 55/61] GwR add composite columns to Merge Comments eligible
types
---
src/calibre/gui2/catalog/catalog_epub_mobi.py | 27 ++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py
index a09cb4119c..94760306c3 100644
--- a/src/calibre/gui2/catalog/catalog_epub_mobi.py
+++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py
@@ -144,6 +144,9 @@ def initialize(self, name, db):
# Hook changes to thumb_width
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
+ # Hook changes to Description section
+ self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
+
def options(self):
# Save/return the current options
# exclude_genre stores literally
@@ -265,7 +268,7 @@ def populateComboBoxes(self):
custom_fields = {}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
- if field_md['datatype'] in ['text','comments']:
+ if field_md['datatype'] in ['text','comments','composite']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Blank field first
@@ -324,6 +327,28 @@ def exclude_source_field_changed(self,new_index):
else:
self.exclude_pattern.setEnabled(False)
+ def generate_descriptions_changed(self,new_state):
+ '''
+ Process changes to Descriptions section
+ 0: unchecked
+ 2: checked
+ '''
+
+ return
+
+ if new_state == 0:
+ # unchecked
+ self.merge_source_field.setEnabled(False)
+ self.merge_before.setEnabled(False)
+ self.merge_after.setEnabled(False)
+ self.include_hr.setEnabled(False)
+ elif new_state == 2:
+ # checked
+ self.merge_source_field.setEnabled(True)
+ self.merge_before.setEnabled(True)
+ self.merge_after.setEnabled(True)
+ self.include_hr.setEnabled(True)
+
def header_note_source_field_changed(self,new_index):
'''
Process changes in the header_note_source_field combo box
From bad6cad84b6bc6d453f6efdf83400f4192206a24 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Fri, 21 Jan 2011 07:55:28 -0500
Subject: [PATCH 56/61] Fix bug #8422: RTF incorrect spacing between letters.
---
src/calibre/ebooks/rtf/rtfml.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py
index 5aa4979b04..1fb14eb06f 100644
--- a/src/calibre/ebooks/rtf/rtfml.py
+++ b/src/calibre/ebooks/rtf/rtfml.py
@@ -262,7 +262,7 @@ def dump_text(self, elem, stylizer, tag_stack=[]):
if hasattr(elem, 'tail') and elem.tail != None and elem.tail.strip() != '':
if 'block' in tag_stack:
- text += '%s ' % txt2rtf(elem.tail)
+ text += '%s' % txt2rtf(elem.tail)
else:
text += '{\\par \\pard \\hyphpar %s}' % txt2rtf(elem.tail)
From 30f65e0ebac0afb344e96a1f15757f65287f1c98 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Fri, 21 Jan 2011 08:51:48 -0500
Subject: [PATCH 57/61] GUI: Regex Test, clear matched items when no regex is
present. Speed up clearing by not running regex matching on empty patterns.
---
src/calibre/gui2/convert/regex_builder.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py
index c5c8d84a88..ca6e429176 100644
--- a/src/calibre/gui2/convert/regex_builder.py
+++ b/src/calibre/gui2/convert/regex_builder.py
@@ -47,6 +47,8 @@ def regex_valid(self):
return False
else:
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
+ self.preview.setExtraSelections([])
+ return False
return True
def do_test(self):
From f22708691379aeac96911e68976b7cdd877fe6c2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 21 Jan 2011 09:46:50 -0700
Subject: [PATCH 58/61] Fix memory leak when switching libraries
---
src/calibre/gui2/actions/choose_library.py | 7 +++++++
src/calibre/gui2/ui.py | 1 +
src/calibre/library/database2.py | 2 ++
3 files changed, 10 insertions(+)
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index a2679c2482..930e5e29aa 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -384,7 +384,14 @@ def switch_requested(self, location):
return
prefs['library_path'] = loc
+ #from calibre.utils.mem import memory
+ #import weakref, gc
+ #ref = weakref.ref(self.gui.library_view.model().db)
+ #before = memory()/1024**2
self.gui.library_moved(loc)
+ #print gc.get_referrers(ref)[0]
+ #for i in xrange(3): gc.collect()
+ #print 'leaked:', memory()/1024**2 - before
def qs_requested(self, idx, *args):
self.switch_requested(self.qs_locations[idx])
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index c6d069cc86..b33c059c9b 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -440,6 +440,7 @@ def library_moved(self, newloc, copy_structure=False):
except:
import traceback
traceback.print_exc()
+ olddb.break_cycles()
if self.device_connected:
self.set_books_in_library(self.booklists(), reset=True)
self.refresh_ondevice()
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 33593e93fe..3dc110c1c8 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -361,6 +361,8 @@ def migrate_preference(key, default):
self.refresh()
self.last_update_check = self.last_modified()
+ def break_cycles(self):
+ self.data = self.field_metadata = self.prefs = self.listeners = None
def initialize_database(self):
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
From 682bf119d0ad77c5a85226cf4a683b9d96724466 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 21 Jan 2011 10:58:31 -0700
Subject: [PATCH 59/61] When converting books in the calibre GUI, override
metadata from the input document. So if you have removed all the tags and
comments in the calibre GUI for the book in the calibre GUI, but the actual
file that is being converted still has tags and comments, they are ignored.
This affects only conversions in the calibre GUI, not from the command line
via ebook-convert. Fixes #8390 ("comments" still showing up on inserted
metadata page, despite deleting the "comments")
---
src/calibre/ebooks/conversion/plumber.py | 7 +++--
src/calibre/ebooks/oeb/transforms/metadata.py | 28 +++++++++++++++++--
src/calibre/gui2/convert/gui_conversion.py | 11 ++++++--
src/calibre/gui2/tools.py | 4 +--
src/calibre/utils/ipc/worker.py | 3 ++
5 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 908ca6a493..dd527ea0b5 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -72,7 +72,8 @@ class Plumber(object):
]
def __init__(self, input, output, log, report_progress=DummyReporter(),
- dummy=False, merge_plugin_recs=True, abort_after_input_dump=False):
+ dummy=False, merge_plugin_recs=True, abort_after_input_dump=False,
+ override_input_metadata=False):
'''
:param input: Path to input file.
:param output: Path to output file/directory
@@ -87,6 +88,7 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
self.log = log
self.ui_reporter = report_progress
self.abort_after_input_dump = abort_after_input_dump
+ self.override_input_metadata = override_input_metadata
# Pipeline options {{{
# Initialize the conversion options that are independent of input and
@@ -924,7 +926,8 @@ def run(self):
self.opts.dest = self.opts.output_profile
from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
- MergeMetadata()(self.oeb, self.user_metadata, self.opts)
+ MergeMetadata()(self.oeb, self.user_metadata, self.opts,
+ override_input_metadata=self.override_input_metadata)
pr(0.2)
self.flush()
diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py
index 4bb25f650e..dc52f3298c 100644
--- a/src/calibre/ebooks/oeb/transforms/metadata.py
+++ b/src/calibre/ebooks/oeb/transforms/metadata.py
@@ -10,7 +10,7 @@
from calibre.utils.date import isoformat, now
from calibre import guess_type
-def meta_info_to_oeb_metadata(mi, m, log):
+def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
from calibre.ebooks.oeb.base import OPF
if not mi.is_null('title'):
m.clear('title')
@@ -29,15 +29,23 @@ def meta_info_to_oeb_metadata(mi, m, log):
if not mi.is_null('book_producer'):
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
m.add('contributor', mi.book_producer, role='bkp')
+ elif override_input_metadata:
+ m.filter('contributor', lambda x : x.role.lower() == 'bkp')
if not mi.is_null('comments'):
m.clear('description')
m.add('description', mi.comments)
+ elif override_input_metadata:
+ m.clear('description')
if not mi.is_null('publisher'):
m.clear('publisher')
m.add('publisher', mi.publisher)
+ elif override_input_metadata:
+ m.clear('publisher')
if not mi.is_null('series'):
m.clear('series')
m.add('series', mi.series)
+ elif override_input_metadata:
+ m.clear('series')
if not mi.is_null('isbn'):
has = False
for x in m.identifier:
@@ -46,19 +54,27 @@ def meta_info_to_oeb_metadata(mi, m, log):
has = True
if not has:
m.add('identifier', mi.isbn, scheme='ISBN')
+ elif override_input_metadata:
+ m.filter('identifier', lambda x: x.scheme.lower() == 'isbn')
if not mi.is_null('language'):
m.clear('language')
m.add('language', mi.language)
if not mi.is_null('series_index'):
m.clear('series_index')
m.add('series_index', mi.format_series_index())
+ elif override_input_metadata:
+ m.clear('series_index')
if not mi.is_null('rating'):
m.clear('rating')
m.add('rating', '%.2f'%mi.rating)
+ elif override_input_metadata:
+ m.clear('rating')
if not mi.is_null('tags'):
m.clear('subject')
for t in mi.tags:
m.add('subject', t)
+ elif override_input_metadata:
+ m.clear('subject')
if not mi.is_null('pubdate'):
m.clear('date')
m.add('date', isoformat(mi.pubdate))
@@ -68,9 +84,14 @@ def meta_info_to_oeb_metadata(mi, m, log):
if not mi.is_null('rights'):
m.clear('rights')
m.add('rights', mi.rights)
+ elif override_input_metadata:
+ m.clear('rights')
if not mi.is_null('publication_type'):
m.clear('publication_type')
m.add('publication_type', mi.publication_type)
+ elif override_input_metadata:
+ m.clear('publication_type')
+
if not m.timestamp:
m.add('timestamp', isoformat(now()))
@@ -78,11 +99,12 @@ def meta_info_to_oeb_metadata(mi, m, log):
class MergeMetadata(object):
'Merge in user metadata, including cover'
- def __call__(self, oeb, mi, opts):
+ def __call__(self, oeb, mi, opts, override_input_metadata=False):
self.oeb, self.log = oeb, oeb.log
m = self.oeb.metadata
self.log('Merging user specified metadata...')
- meta_info_to_oeb_metadata(mi, m, oeb.log)
+ meta_info_to_oeb_metadata(mi, m, oeb.log,
+ override_input_metadata=override_input_metadata)
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
m.clear('cover')
if cover_id is not None:
diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py
index d1e1270924..ce51d4ca77 100644
--- a/src/calibre/gui2/convert/gui_conversion.py
+++ b/src/calibre/gui2/convert/gui_conversion.py
@@ -12,17 +12,24 @@
from calibre.utils.logging import Log
def gui_convert(input, output, recommendations, notification=DummyReporter(),
- abort_after_input_dump=False, log=None):
+ abort_after_input_dump=False, log=None, override_input_metadata=False):
recommendations = list(recommendations)
recommendations.append(('verbose', 2, OptionRecommendation.HIGH))
if log is None:
log = Log()
plumber = Plumber(input, output, log, report_progress=notification,
- abort_after_input_dump=abort_after_input_dump)
+ abort_after_input_dump=abort_after_input_dump,
+ override_input_metadata=override_input_metadata)
plumber.merge_ui_recommendations(recommendations)
plumber.run()
+def gui_convert_override(input, output, recommendations, notification=DummyReporter(),
+ abort_after_input_dump=False, log=None):
+ gui_convert(input, output, recommendations, notification=notification,
+ abort_after_input_dump=abort_after_input_dump, log=log,
+ override_input_metadata=True)
+
def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device,
notification=DummyReporter(), log=None):
if log is None:
diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py
index a5f0f52425..50c384b24c 100644
--- a/src/calibre/gui2/tools.py
+++ b/src/calibre/gui2/tools.py
@@ -75,7 +75,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
temp_files.append(d.cover_file)
args = [in_file, out_file.name, recs]
temp_files.append(out_file)
- jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files))
+ jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
changed = True
d.break_cycles()
@@ -185,7 +185,7 @@ def do_book(self):
args = [in_file, out_file.name, lrecs]
temp_files.append(out_file)
- self.jobs.append(('gui_convert', args, desc, self.output_format.upper(), book_id, temp_files))
+ self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
self.changed = True
self.setValue(self.i)
diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py
index d8ffad7c53..e187235a9e 100644
--- a/src/calibre/utils/ipc/worker.py
+++ b/src/calibre/utils/ipc/worker.py
@@ -28,6 +28,9 @@
'gui_convert' :
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
+ 'gui_convert_override' :
+ ('calibre.gui2.convert.gui_conversion', 'gui_convert_override', 'notification'),
+
'gui_catalog' :
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
From 1e2501e8da9bc3c6310af41c4a969edf22fa170d Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 21 Jan 2011 11:01:57 -0700
Subject: [PATCH 60/61] ...
---
resources/recipes/ibm_smarter_planet.recipe | 1 +
resources/recipes/karlsruhe.recipe | 1 +
resources/recipes/kath_net.recipe | 1 +
resources/recipes/nationalgeographic.recipe | 1 +
4 files changed, 4 insertions(+)
diff --git a/resources/recipes/ibm_smarter_planet.recipe b/resources/recipes/ibm_smarter_planet.recipe
index 2e5c46fb80..44978142f6 100644
--- a/resources/recipes/ibm_smarter_planet.recipe
+++ b/resources/recipes/ibm_smarter_planet.recipe
@@ -5,6 +5,7 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
__author__ = 'Jack Mason'
author = 'IBM Global Business Services'
publisher = 'IBM'
+ language = 'en'
category = 'news, technology, IT, internet of things, analytics'
oldest_article = 7
max_articles_per_feed = 30
diff --git a/resources/recipes/karlsruhe.recipe b/resources/recipes/karlsruhe.recipe
index c0bc5369f1..182d8e532b 100644
--- a/resources/recipes/karlsruhe.recipe
+++ b/resources/recipes/karlsruhe.recipe
@@ -6,6 +6,7 @@ class KANewsRecipe(BasicNewsRecipe):
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
__author__ = 'tfeld'
lang='de'
+ language = 'de'
no_stylesheets = True
oldest_article = 7
diff --git a/resources/recipes/kath_net.recipe b/resources/recipes/kath_net.recipe
index 7c469adbe8..933876c2a8 100644
--- a/resources/recipes/kath_net.recipe
+++ b/resources/recipes/kath_net.recipe
@@ -4,6 +4,7 @@ class AdvancedUserRecipe1295262156(BasicNewsRecipe):
title = u'kath.net'
__author__ = 'Bobus'
oldest_article = 7
+ language = 'en'
max_articles_per_feed = 100
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
diff --git a/resources/recipes/nationalgeographic.recipe b/resources/recipes/nationalgeographic.recipe
index f00c9206bd..44aac314f9 100644
--- a/resources/recipes/nationalgeographic.recipe
+++ b/resources/recipes/nationalgeographic.recipe
@@ -10,6 +10,7 @@
class NationalGeographicNews(BasicNewsRecipe):
title = u'National Geographic News'
oldest_article = 7
+ language = 'en'
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
From ceb28df1b0d0ef459e4c5b80727a5c774e7c7981 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Fri, 21 Jan 2011 11:20:08 -0700
Subject: [PATCH 61/61] Fix #8271 (Tag editor forgets position)
---
src/calibre/gui2/dialogs/tag_editor.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/calibre/gui2/dialogs/tag_editor.py b/src/calibre/gui2/dialogs/tag_editor.py
index c12b7357f1..d4325354a1 100644
--- a/src/calibre/gui2/dialogs/tag_editor.py
+++ b/src/calibre/gui2/dialogs/tag_editor.py
@@ -79,6 +79,8 @@ def delete_tags(self, item=None):
def apply_tags(self, item=None):
items = self.available_tags.selectedItems() if item is None else [item]
+ rows = [self.available_tags.row(i) for i in items]
+ row = max(rows)
for item in items:
tag = unicode(item.text())
self.tags.append(tag)
@@ -89,6 +91,12 @@ def apply_tags(self, item=None):
for tag in self.tags:
self.applied_tags.addItem(tag)
+ if row >= self.available_tags.count():
+ row = self.available_tags.count() - 1
+
+ if row > 2:
+ item = self.available_tags.item(row)
+ self.available_tags.scrollToItem(item)
def unapply_tags(self, item=None):