IGN:Sync with trunk

This commit is contained in:
Kovid Goyal 2008-08-16 10:18:58 -07:00
commit d832563871
84 changed files with 8510 additions and 5187 deletions

View File

@ -10,7 +10,6 @@ import glob, sys, subprocess, tarfile, os, re, py_compile, shutil
HOME = '/home/kovid'
PYINSTALLER = os.path.expanduser('~/build/pyinstaller')
CALIBREPREFIX = '___'
CLIT = '/usr/bin/clit'
PDFTOHTML = '/usr/bin/pdftohtml'
LIBUNRAR = '/usr/lib/libunrar.so'
QTDIR = '/usr/lib/qt4'
@ -20,7 +19,9 @@ SQLITE = '/usr/lib/libsqlite3.so.0'
DBUS = '/usr/lib/libdbus-1.so.3'
LIBMNG = '/usr/lib/libmng.so.1'
LIBZ = '/lib/libz.so.1'
LIBUSB = '/lib/libusb.so'
LIBBZ2 = '/lib/libbz2.so.1'
LIBUSB = '/usr/lib/libusb.so'
LIBPOPPLER = '/usr/lib/libpoppler.so.3'
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
@ -115,11 +116,12 @@ for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so.*')):
binaries += [(os.path.basename(f), f, 'BINARY')]
print 'Adding external programs...'
binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'),
binaries += [('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')]
print 'Adding external libraries...'
binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG, LIBZ, LIBUSB)]
binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS,
LIBMNG, LIBZ, LIBBZ2, LIBUSB, LIBPOPPLER)]
qt = []

View File

@ -243,12 +243,6 @@ _check_symlinks_prescript()
self.add_plugins()
print
print 'Adding clit'
os.link(os.path.expanduser('~/clit'), os.path.join(frameworks_dir, 'clit'))
print
print 'Adding unrtf'
os.link(os.path.expanduser('~/unrtf'), os.path.join(frameworks_dir, 'unrtf'))
print
print 'Adding pdftohtml'
os.link(os.path.expanduser('~/pdftohtml'), os.path.join(frameworks_dir, 'pdftohtml'))

View File

@ -213,11 +213,9 @@ File ::D9A3AF75-5939-CB51-9F33-5A048911103E -type dir -name etc -parent 6CCF3F71
File ::A628E495-239B-DAF4-D858-BCE36CB41E6E -type dir -name imageformats -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0A533DB2-D494-A9ED-1334-DECC357BD426 -type dir -name codecs -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0F47A44E-E347-1CD4-E89F-37B447C4A270 -type dir -name driver -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::19A416A8-8DDD-658B-A3D4-F89ABD02CFBB -type dir -name ImageMagick -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A146565C-D163-7F68-7C70-A6A336B32526 -type dir -name iconengines -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3245B06C-1C22-1A8A-5710-6D36651AAA70 -name etree.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B49A5610-13F6-FB5D-0673-DB47C6BB385D -name rtf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1F7D32C6-D61D-A09F-6D9E-690F7DD10D04 -name clit.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CE4F2A21-12CC-2B9A-6D48-6A0FEA7C9D13 -name _compiled_base.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D6C46340-8335-7FC4-A027-D701DF1B70AB -name pdf2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::53F2B07D-8F92-2328-C55E-5F7F0E63D5DB -name opf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
@ -348,13 +346,150 @@ File ::ACE1537B-B234-3C90-759A-8947A7AADC77 -name mobi2oeb.exe -parent 6CCF3F71-
File ::92701E8F-1D91-A796-C899-2A266029F61D -name _socket.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::45BD27B5-B910-7633-C827-37E82E89C27C -name w9xpopen.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::45C27909-D761-787F-84B2-66596E5C4E99 -name bz2.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7B2DE5D8-17A6-B167-ABC7-799AEBCC1C02 -name clit.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2 -type dir -name plugins -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::71930E14-A27B-C23C-8D94-C7E97ADB8723 -name pictureflow.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::293E6ABE-17C9-5E53-1B44-C27029C8C061 -name winutil.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::A5737158-18DF-7F20-2BDF-2DF615663891 -name lzx.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::CA9E098C-2931-9781-1303-213C242F9A5E -name lit2oeb.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::16B5A447-066C-C93E-F63D-8BC0D57CA544 -name lit2oeb.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ABF342D2-82A9-2A20-BA97-54AD5BAF1A2A -name IM_MOD_RL_sfw_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3877E295-C7EB-DF63-DA91-B9E2F9D8035A -name comic2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F47345A3-6CE0-4F5B-9CAE-3F4A18D4AA5A -name IM_MOD_RL_rle_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3A211C93-1B8B-A8AA-E240-A3287974DAB4 -name IM_MOD_RL_tiff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F24F3123-05A3-B452-D12B-CE6C126501B1 -name CORE_RL_libxml_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D7034E85-2733-DB61-DB49-C34B767B4B45 -name IM_MOD_RL_sun_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::21C1F4D3-487E-5FFF-C8CE-8E5FE779A786 -name IM_MOD_RL_msl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::01EC7979-C9CB-696C-E8B3-F5945E1115BB -name IM_MOD_RL_jp2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0F86B693-D83A-DB03-8641-219FE766D980 -name CORE_RL_ttf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DDCAEB76-7AC4-CA1A-0742-34B556592D2A -name IM_MOD_RL_exr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4D274537-6B6D-63F2-2615-E0CD279880A3 -name CORE_RL_Magick++_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A87CE00E-9F87-DBE3-DDA5-FC68D6D0731E -name IM_MOD_RL_dib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9114D530-B73B-CC7C-F6A6-655B7271AB35 -name IM_MOD_RL_preview_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::31EE0880-F1C8-94D6-4EDC-B09A576E0908 -name CORE_RL_magick_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BEA2B769-1A54-4398-E8B4-5BE15637B705 -name IM_MOD_RL_svg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7E300AD4-7C03-5835-0DD6-E9FA8737585A -name IM_MOD_RL_mpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5E9901D8-BBB7-A17C-5A04-837C0ADF8CAE -name IM_MOD_RL_clipboard_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F13497D2-87C0-243D-916A-0A160F1A1896 -name IM_MOD_RL_png_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DBD1CF95-1B01-9F5C-66D9-C7B4E1B44CC7 -name CORE_RL_bzlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::149C038D-9CD6-20C5-49C3-FC6948D0709D -name IM_MOD_RL_wmf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F77F5E54-1D54-F7D3-9520-BB1811C11AA6 -name IM_MOD_RL_txt_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3545B38D-1BDF-B355-F779-4D83F292E2B6 -name IM_MOD_RL_viff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6E33F2FD-17BB-F096-4551-0E3B22924A4D -name IM_MOD_RL_ps2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F4C810FF-4291-4491-0FA2-CFAD0BA690A9 -name type.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::02A3CD7D-743C-FAA8-9C20-3E8E59B8C2C2 -name IM_MOD_RL_ps3_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::697020D6-C5DA-A7DC-9454-1F9523D7748D -name IM_MOD_RL_dot_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E9D0609E-2D12-A8C0-9B47-D09CACB4A3AF -name IM_MOD_RL_xwd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F237A6C8-4037-B9E5-8D65-29A5A69CADFE -name IM_MOD_RL_fits_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BDB45C50-E57A-357D-1D5A-392036227E6B -name IM_MOD_RL_histogram_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5A6DBEB5-CD8A-4109-A04C-EF0436BC1CDC -name mfc71.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FCC2A44D-D2F9-74DC-0C27-86F094E2C3E9 -name IM_MOD_RL_pnm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8E71473E-34AE-B7A3-B506-8A6AA622DAD7 -name IM_MOD_RL_ipl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B680E84A-BA1C-5EA2-902E-095DD22A48F2 -name msvcp71.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6AF09D1D-8889-8A87-9FD4-1471DBB1354C -name IM_MOD_RL_rgb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::941B86E2-428A-3F4A-EB34-CBDBDDAD648C -name IM_MOD_RL_xbm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1613269D-8A63-C843-E862-9B80CC17E60F -name IM_MOD_RL_otb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::334E3925-703D-DDCA-A079-C53DB06AA069 -name IM_MOD_RL_avi_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::23549B03-F856-3B90-C9C5-3B64A5910C7B -name CORE_RL_lcms_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0E3F5727-D99A-44CD-35E0-4FDFBB95FCBC -name IM_MOD_RL_xpm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::955B5799-4DB3-F422-589A-CDC20A82B6CB -name IM_MOD_RL_xcf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D3AB494C-3218-0137-4399-3FB1662C05D3 -name IM_MOD_RL_emf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1DEF5AF0-2376-539B-2A61-35B6ADC2F4BA -name log.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D472B449-3644-C538-30EF-EC42E3B84C43 -name IM_MOD_RL_mtv_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8B4E61F1-8FC2-7E65-4B94-3F19100DF58B -name CORE_RL_tiff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B747FE2A-0054-6815-40D0-74F89FC8C757 -name IM_MOD_RL_dps_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8B1660CC-7A97-96A2-1280-34554028CB9F -name IM_MOD_RL_dcm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::21B7EBEC-30C8-F2E8-9D73-E4E6965EA856 -name locale.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::85087BFC-42D6-C583-586E-19CAD45E6A61 -name CORE_RL_wand_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BE24EB64-E7BB-0E63-256E-DEDC2BBF1C2B -name IM_MOD_RL_ttf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7693E752-1A81-F6F3-C55D-9E8D94D6E4DC -name IM_MOD_RL_dpx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A1451D28-A06B-3F03-4DCA-884729C5A030 -name IM_MOD_RL_jpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3F0D8F7A-906F-8CAE-84D7-E3480A09D39D -name IM_MOD_RL_fax_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CA8F1852-F5C1-86E8-31B9-8B1EFE837ADB -name IM_MOD_RL_avs_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::72370AAC-67CF-F570-2AA2-658E4C81C859 -name IM_MOD_RL_mvg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8265A1E-E9B5-A38F-9ACF-99669CAE1E9F -name IM_MOD_RL_tga_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8A9A383-0364-515F-C1D8-F82C274D652B -name configure.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A2167661-AF2B-E15E-60DA-715F47E5AA30 -name IM_MOD_RL_uil_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7608E2FF-1BF6-E18A-A884-244794BDA01B -name IM_MOD_RL_cut_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::772CA344-5EFD-78A0-3542-777F12356C8D -name Xext.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::37F8D85E-4EE9-80E5-A4A2-8F30444AD5CC -name IM_MOD_RL_scr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::973011B9-D193-6D64-D4EB-D82B0C730379 -name IM_MOD_RL_map_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9B9F088C-A20A-0C19-EF7D-52908A020D36 -name thresholds.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::831AAF1C-7CBE-CAD3-79A8-7430E8DE484E -name IM_MOD_RL_thumbnail_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::85236603-D71F-359C-B235-98C77809DDF1 -name IM_MOD_RL_mpc_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::94300805-117F-8337-A9BF-41E10D8AB437 -name IM_MOD_RL_cmyk_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::618A299D-4A28-E37A-D4BF-9209B594FAAF -name IM_MOD_RL_pcd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9F6572D8-6BE6-290B-D4A7-A0D4E4DBAC23 -name IM_MOD_RL_sct_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::942E63AC-F579-0D17-FF56-E2C8CC5DECA3 -name IM_MOD_RL_pict_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F68DE3F9-742C-D8EE-B2FC-FF9B37EED8F3 -name IM_MOD_RL_gradient_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C460E29B-38EE-6FC0-757B-69563EFC3225 -name IM_MOD_RL_icon_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AE00BD3D-734C-78F6-9078-C04749F4652A -name X11.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B70ED455-A480-56E3-3BDE-E06CDDB62C04 -name IM_MOD_RL_jbig_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CFFC9A5D-2902-FD37-DBD1-6800C7C0C1AE -name magic.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::32DA3775-410C-0391-7ADB-B58028CC04E2 -name IM_MOD_RL_mat_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AB64F079-1F8D-BE3A-731B-4B20ABD20289 -name IM_MOD_RL_meta_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5A7F49E9-119A-FD9B-8186-0BE6B9DCF210 -name IM_MOD_RL_gray_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D92B4157-F307-64A4-9AA5-C5AA1F138E1B -name IM_MOD_RL_pwp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BA631BF0-CB17-D0EC-FAA9-D7B426457DD3 -name IM_MOD_RL_fpx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9EA95108-72D5-13B5-2BD4-87CECED9B367 -name IM_MOD_RL_pcl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3111AC7E-2387-AD7D-253F-979195AC4EA1 -name english.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::14C1E910-6F5D-9540-7430-6B0B92311EB2 -name IM_MOD_RL_wpg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5D7050F4-177A-03A2-3DD1-A7DFC968E4ED -name IM_MOD_RL_pdb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5BACE29D-FAFD-E673-16A9-D22DCE6E0655 -name IM_MOD_RL_label_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::25F84452-26F7-4305-B405-B1D0C7D072D2 -name IM_MOD_RL_clip_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::807E6FF7-2D61-F308-BA2A-BD07A213078A -name IM_MOD_RL_pix_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1A951976-DBCC-9FAE-190C-B24BBA38A97A -name colors.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8608BB2C-6CDE-BBE7-39C6-DF83625D5BFB -name IM_MOD_RL_cin_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CBE1DFDA-7E32-759F-346E-DD469B1CE1F0 -name IM_MOD_RL_bmp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7AD432A3-5146-4966-8C8E-85ACDCC8CA7A -name IM_MOD_RL_raw_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9DC22033-0F40-26CC-9E09-959738F62855 -name IM_MOD_RL_cip_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8C777EC-AEAA-6B3F-22A6-CEC28A2E5058 -name IM_MOD_RL_pdf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ACA1A829-27AE-EFE7-4EDD-01D050A2E0A6 -name analyze.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C883300E-0C2A-EAF6-D72E-81E8B99535E1 -name IM_MOD_RL_mpr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A223D40F-EFC5-31E3-8E33-B90984080A3E -name CORE_RL_jpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DE197248-9758-A368-6058-B72C5169E0DD -name IM_MOD_RL_wbmp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FD15A9C0-5C14-11CA-AA27-D66D638E58FC -name IM_MOD_RL_stegano_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6668D58B-E040-328B-4AF4-14C738C172BA -name CORE_RL_jp2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::871E464B-4566-1FC2-55CB-B65AEB416413 -name IM_MOD_RL_yuv_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7D7A8325-4C69-B9D3-C832-803BCF999B5C -name coder.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B0A3651D-19B1-09F1-8197-1E58ED2CC704 -name IM_MOD_RL_null_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AB877243-6DAE-BF0C-70C2-F2D702B16231 -name IM_MOD_RL_pattern_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F571F366-1737-7E65-5441-DEBD166DE247 -name IM_MOD_RL_plasma_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::93F18CDE-B871-B2D4-3C0F-7C1B933E1ACB -name IM_MOD_RL_pcx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D1567C76-29D0-C200-9FC7-F7E1399D3011 -name CORE_RL_xlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BEF1E1AC-9564-EA49-2B8F-1AAC9F6A7669 -name IM_MOD_RL_caption_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::2D14864E-6A39-FE03-4EA8-CCE7AC94487D -name comic2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::27CD65A5-D5F9-C982-5096-65298417EE61 -name IM_MOD_RL_url_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A273E901-0B63-390B-D44A-7240491C6F59 -name IM_MOD_RL_info_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B39B27EC-325A-D222-01FC-F6B3BC92E99A -name IM_MOD_RL_hdf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BA634D39-716B-C895-73DD-2E5FA3CA2F9C -name CORE_RL_png_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::35560FB0-A7BD-54C7-C799-3EB2922BED2C -name delegates.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FCE51670-E4AE-B813-6CFC-A7A9B627F72C -name IM_MOD_RL_matte_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D10DB719-887D-4898-DAA8-8F1C6A4203B2 -name IM_MOD_RL_mono_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::78A66F97-ECEC-BFEC-75F2-2FA2087927C2 -name CORE_RL_jbig_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FB74C41B-3F08-A9E8-B38D-C7C2FDFE9560 -name IM_MOD_RL_xtrn_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ABC0A7AF-B14B-09BE-4756-76C8FE771517 -name IM_MOD_RL_vicar_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F69B9AAD-EC2B-5EC7-5ED8-1395033DE0F5 -name IM_MOD_RL_psd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::66CB1D9D-9995-F71C-155D-F1F4AA3B6D40 -name IM_MOD_RL_uyvy_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4DB66BC3-4C48-C763-9BCA-9E831CA1FF0B -name IM_MOD_RL_art_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EB24F574-4226-6404-B069-7B46C04988E0 -name IM_MOD_RL_ycbcr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A9990A18-D6A1-AA14-1EDF-FC43D8AE0C7E -name type-ghostscript.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5B72558A-192B-76EB-1BA8-C4CBA43C6C05 -name IM_MOD_RL_x_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A3937F85-1D17-D3DD-2DF5-FB9FE4A99ADB -name IM_MOD_RL_dng_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B2C41CC1-EB2D-F7E7-B22E-0C154C4C96C1 -name IM_MOD_RL_html_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::68A62902-7F48-6E7A-E5D3-1F58C895B409 -name IM_MOD_RL_tim_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E5C83E45-56B1-9BD7-7676-07CABD98E0BF -name IM_MOD_RL_tile_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::084206D9-98DB-DE2A-19BC-FD17A191096D -name IM_MOD_RL_xc_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::43BE7C18-6369-E035-8390-2E13C8CBB33C -name CORE_RL_zlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EFC4D6E5-4FC9-25D5-B308-8CC8C13EF3A1 -name IM_MOD_RL_gif_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::05A8646B-F100-4803-5916-4CBAC154BFE9 -name IM_MOD_RL_sgi_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1B354F22-4795-739A-A47D-8F2D99DFB58A -name IM_MOD_RL_ept_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B6725A29-1F09-2982-6BE1-29062A90F684 -name IM_MOD_RL_palm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::445BD28F-0E70-B452-15B3-9E0C353CE345 -name IM_MOD_RL_ps_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::183A1789-2ED2-D555-AE4B-B7EBC97EB1D5 -name IM_MOD_RL_miff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::69178142-77D3-D7C5-74C7-6F1597474123 -name IM_MOD_RL_vid_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9D84C810-6DEC-5831-CFC6-AD0543D95881 -name IM_MOD_RL_rla_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::514DCC61-0BE9-6C5C-A970-170219D3A87E -name IM_MOD_RL_magick_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AAF94AED-250D-DE8D-14C5-FA8BC05AAE74 -name IM_MOD_RL_djvu_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
Component ::F6829AB7-9F66-4CEE-CA0E-21F54C6D3609 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Main -parent Components
SetupType ::D9ADE41C-B744-690C-2CED-CF826BF03D2E -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Typical -parent SetupTypes

View File

@ -10,7 +10,6 @@ QT_DIR = 'C:\\Qt\\4.4.0'
DEVCON = 'C:\\devcon\\i386\\devcon.exe'
LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
CLIT = 'C:\\clit\\clit.exe'
PDFTOHTML = 'C:\\pdftohtml\\pdftohtml.exe'
IMAGEMAGICK_DIR = 'C:\\ImageMagick'
FONTCONFIG_DIR = 'C:\\fontconfig'
@ -105,8 +104,6 @@ class BuildEXE(py2exe.build_exe.py2exe):
shutil.copyfile(DEVCON, os.path.join(tdir, os.path.basename(DEVCON)))
print '\tAdding unrar'
shutil.copyfile(LIBUNRAR, os.path.join(PY2EXE_DIR, os.path.basename(LIBUNRAR)))
print '\tAdding ConvertLIT'
shutil.copyfile(CLIT, os.path.join(PY2EXE_DIR, os.path.basename(CLIT)))
print '\tAdding pdftohtml'
shutil.copyfile(PDFTOHTML, os.path.join(PY2EXE_DIR, os.path.basename(PDFTOHTML)))
print '\tAdding ImageMagick'

196
pyqtdistutils.py Normal file
View File

@ -0,0 +1,196 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Build PyQt extensions. Integrates with distutils (but uses the PyQt build system).
'''
from distutils.core import Extension
from distutils.command.build_ext import build_ext as _build_ext
from distutils.dep_util import newer_group
from distutils import log
import sipconfig, os, sys, string, glob, shutil
from PyQt4 import pyqtconfig
iswindows = 'win32' in sys.platform
QMAKE = os.path.expanduser('~/qt/bin/qmake') if 'darwin' in sys.platform else'qmake'
WINDOWS_PYTHON = ['C:/Python25/libs']
OSX_SDK = '/Developer/SDKs/MacOSX10.4u.sdk'
def replace_suffix(path, new_suffix):
return os.path.splitext(path)[0] + new_suffix
class PyQtExtension(Extension):
def __init__(self, name, sources, sip_sources, **kw):
'''
:param sources: Qt .cpp and .h files needed for this extension
:param sip_sources: List of .sip files this extension depends on. The
first .sip file will be used toactually build the extension.
'''
self.module_makefile = pyqtconfig.QtGuiModuleMakefile
self.sip_sources = map(lambda x: x.replace('/', os.sep), sip_sources)
Extension.__init__(self, name, sources, **kw)
class build_ext(_build_ext):
def make(self, makefile):
make = 'make'
if iswindows:
make = 'mingw32-make'
self.spawn([make, '-f', makefile])
def build_qt_objects(self, ext, bdir):
if not iswindows:
bdir = os.path.join(bdir, 'qt')
if not os.path.exists(bdir):
os.makedirs(bdir)
cwd = os.getcwd()
sources = map(os.path.abspath, ext.sources)
os.chdir(bdir)
try:
headers = set([f for f in sources if f.endswith('.h')])
sources = set(sources) - headers
name = ext.name.rpartition('.')[-1]
pro = '''\
TARGET = %s
TEMPLATE = lib
HEADERS = %s
SOURCES = %s
VERSION = 1.0.0
CONFIG += x86 ppc
'''%(name, ' '.join(headers), ' '.join(sources))
open(name+'.pro', 'wb').write(pro)
self.spawn([QMAKE, '-o', 'Makefile.qt', name+'.pro'])
self.make('Makefile.qt')
pat = 'release\\*.o' if iswindows else '*.o'
return map(os.path.abspath, glob.glob(pat))
finally:
os.chdir(cwd)
def build_sbf(self, sip, sbf, bdir):
sip_bin = self.sipcfg.sip_bin
self.spawn([sip_bin,
"-c", bdir,
"-b", sbf,
'-I', self.pyqtcfg.pyqt_sip_dir,
] + self.pyqtcfg.pyqt_sip_flags.split()+
[sip])
def build_pyqt(self, bdir, sbf, ext, qtobjs, headers):
makefile = ext.module_makefile(configuration=self.pyqtcfg,
build_file=sbf, dir=bdir,
makefile='Makefile.pyqt',
universal=OSX_SDK, qt=1)
if 'win32' in sys.platform:
makefile.extra_lib_dirs += WINDOWS_PYTHON
makefile.extra_include_dirs = list(set(map(os.path.dirname, headers)))
makefile.extra_lflags += qtobjs
makefile.generate()
cwd = os.getcwd()
os.chdir(bdir)
try:
self.make('Makefile.pyqt')
finally:
os.chdir(cwd)
def build_extension(self, ext):
self.inplace = True # Causes extensions to be built in the source tree
if not isinstance(ext, PyQtExtension):
return _build_ext.build_extension(self, ext)
fullname = self.get_ext_fullname(ext.name)
if self.inplace:
# ignore build-lib -- put the compiled extension into
# the source tree along with pure Python modules
modpath = string.split(fullname, '.')
package = string.join(modpath[0:-1], '.')
base = modpath[-1]
build_py = self.get_finalized_command('build_py')
package_dir = build_py.get_package_dir(package)
ext_filename = os.path.join(package_dir,
self.get_ext_filename(base))
else:
ext_filename = os.path.join(self.build_lib,
self.get_ext_filename(fullname))
bdir = os.path.abspath(os.path.join(self.build_temp, fullname))
if not os.path.exists(bdir):
os.makedirs(bdir)
ext.sources = map(os.path.abspath, ext.sources)
qt_dir = 'qt\\release' if iswindows else 'qt'
objects = set(map(lambda x: os.path.join(bdir, qt_dir, replace_suffix(os.path.basename(x), '.o')),
[s for s in ext.sources if not s.endswith('.h')]))
newer = False
for object in objects:
if newer_group(ext.sources, object, missing='newer'):
newer = True
break
headers = [f for f in ext.sources if f.endswith('.h')]
if self.force or newer:
log.info('building \'%s\' extension', ext.name)
objects = self.build_qt_objects(ext, bdir)
self.sipcfg = sipconfig.Configuration()
self.pyqtcfg = pyqtconfig.Configuration()
sbf_sources = []
for sip in ext.sip_sources:
sipbasename = os.path.basename(sip)
sbf = os.path.join(bdir, replace_suffix(sipbasename, ".sbf"))
sbf_sources.append(sbf)
if self.force or newer_group(ext.sip_sources, sbf, 'newer'):
self.build_sbf(sip, sbf, bdir)
generated_sources = []
for sbf in sbf_sources:
generated_sources += self.get_sip_output_list(sbf, bdir)
depends = generated_sources + list(objects)
mod = os.path.join(bdir, os.path.basename(ext_filename))
if self.force or newer_group(depends, mod, 'newer'):
self.build_pyqt(bdir, sbf_sources[0], ext, list(objects), headers)
if self.force or newer_group([mod], ext_filename, 'newer'):
if os.path.exists(ext_filename):
os.unlink(ext_filename)
shutil.copyfile(mod, ext_filename)
shutil.copymode(mod, ext_filename)
def get_sip_output_list(self, sbf, bdir):
"""
Parse the sbf file specified to extract the name of the generated source
files. Make them absolute assuming they reside in the temp directory.
"""
for L in file(sbf):
key, value = L.split("=", 1)
if key.strip() == "sources":
out = []
for o in value.split():
out.append(os.path.join(bdir, o))
return out
raise RuntimeError, "cannot parse SIP-generated '%s'" % sbf
def run_sip(self, sip_files):
sip_bin = self.sipcfg.sip_bin
sip_sources = [i[0] for i in sip_files]
generated_sources = []
for sip, sbf in sip_files:
if not (self.force or newer_group(sip_sources, sbf, 'newer')):
log.info(sbf + ' is up to date')
continue
self.spawn([sip_bin,
"-c", self.build_temp,
"-b", sbf,
'-I', self.pyqtcfg.pyqt_sip_dir,
] + self.pyqtcfg.pyqt_sip_flags.split()+
[sip])
generated_sources += self.get_sip_output_list(sbf)
return generated_sources

View File

@ -7,7 +7,7 @@ sys.path.append('src')
iswindows = re.search('win(32|64)', sys.platform)
isosx = 'darwin' in sys.platform
islinux = not isosx and not iswindows
src = open('src/calibre/__init__.py', 'rb').read()
src = open('src/calibre/constants.py', 'rb').read()
VERSION = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1)
APPNAME = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1)
print 'Setup', APPNAME, 'version:', VERSION
@ -47,17 +47,25 @@ main_functions = {
if __name__ == '__main__':
from setuptools import setup, find_packages, Extension
from pyqtdistutils import PyQtExtension, build_ext
import subprocess, glob
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
ext_modules = [Extension('calibre.plugins.lzx',
ext_modules = [
Extension('calibre.plugins.lzx',
sources=['src/calibre/utils/lzx/lzxmodule.c',
'src/calibre/utils/lzx/lzxd.c'],
include_dirs=['src/calibre/utils/lzx']),
Extension('calibre.plugins.msdes',
sources=['src/calibre/utils/msdes/msdesmodule.c',
'src/calibre/utils/msdes/des.c'],
include_dirs=['src/calibre/utils/msdes'])]
include_dirs=['src/calibre/utils/msdes']),
PyQtExtension('calibre.plugins.pictureflow',
['src/calibre/gui2/pictureflow/pictureflow.cpp',
'src/calibre/gui2/pictureflow/pictureflow.h'],
['src/calibre/gui2/pictureflow/pictureflow.sip']
)
]
if iswindows:
ext_modules.append(Extension('calibre.plugins.winutil',
sources=['src/calibre/utils/windows/winutil.c'],
@ -69,42 +77,6 @@ if __name__ == '__main__':
sources=['src/calibre/devices/usbobserver/usbobserver.c'])
)
def build_PyQt_extension(path):
pro = glob.glob(os.path.join(path, '*.pro'))[0]
raw = open(pro).read()
base = qtplugin = re.search(r'TARGET\s*=\s*(.*)', raw).group(1)
ver = re.search(r'VERSION\s*=\s*(\d+)', raw).group(1)
cwd = os.getcwd()
os.chdir(os.path.dirname(pro))
try:
if not os.path.exists('.build'):
os.mkdir('.build')
os.chdir('.build')
subprocess.check_call(( (os.path.expanduser('~/qt/bin/qmake') if isosx else 'qmake'), '..'+os.sep+os.path.basename(pro)))
subprocess.check_call(['mingw32-make' if iswindows else 'make'])
os.chdir(os.path.join('..', 'PyQt'))
if not os.path.exists('.build'):
os.mkdir('.build')
os.chdir('.build')
python = '/Library/Frameworks/Python.framework/Versions/Current/bin/python' if isosx else 'python'
subprocess.check_call([python, '..'+os.sep+'configure.py'])
subprocess.check_call(['mingw32-make' if iswindows else 'make'])
ext = '.pyd' if iswindows else '.so'
plugin = glob.glob(base+ext)[0]
shutil.copyfile(plugin, os.path.join(cwd, 'src', 'calibre', 'plugins', plugin))
finally:
os.chdir(cwd)
if islinux or isosx:
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
try:
os.readlink(f)
os.unlink(f)
except:
continue
for path in [(os.path.join('src', 'calibre', 'gui2', 'pictureflow'))]:
build_PyQt_extension(path)
setup(
name=APPNAME,
packages = find_packages('src'),
@ -152,7 +124,8 @@ if __name__ == '__main__':
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Hardware :: Hardware Drivers'
]
],
cmdclass = {'build_ext': build_ext},
)
if 'develop' in ' '.join(sys.argv) and islinux:

View File

@ -1,122 +1,28 @@
''' E-book management software'''
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
__version__ = '0.4.80'
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid at kovidgoyal.net>"
__appname__ = 'calibre'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, logging, mechanize, locale, copy, cStringIO, re, subprocess, \
textwrap, atexit, cPickle, codecs, time
from gettext import GNUTranslations
import sys, os, re, logging, time, subprocess, mechanize, atexit
from htmlentitydefs import name2codepoint
from math import floor
from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from logging import Formatter
from PyQt4.QtCore import QSettings, QVariant, QUrl, QByteArray, QString
from PyQt4.QtGui import QDesktopServices
from calibre.translations.msgfmt import make
from calibre.ebooks.chardet import detect
from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx)
isfrozen = hasattr(sys, 'frozen')
try:
locale.setlocale(locale.LC_ALL, '')
except:
dl = locale.getdefaultlocale()
try:
if dl:
locale.setlocale(dl[0])
except:
pass
try:
preferred_encoding = locale.getpreferredencoding()
codecs.lookup(preferred_encoding)
except:
preferred_encoding = 'utf-8'
if getattr(sys, 'frozen', False):
if iswindows:
plugin_path = os.path.join(os.path.dirname(sys.executable), 'plugins')
elif isosx:
plugin_path = os.path.join(getattr(sys, 'frameworks_dir'), 'plugins')
elif islinux:
plugin_path = os.path.join(getattr(sys, 'frozen_path'), 'plugins')
sys.path.insert(0, plugin_path)
else:
import pkg_resources
plugins = getattr(pkg_resources, 'resource_filename')(__appname__, 'plugins')
sys.path.insert(0, plugins)
if iswindows and getattr(sys, 'frozen', False):
sys.path.insert(1, os.path.dirname(sys.executable))
from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QDesktopServices
from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfrozen, \
terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl
plugins = {}
for plugin in ['pictureflow', 'lzx', 'msdes'] + \
(['winutil'] if iswindows else []) + \
(['usbobserver'] if isosx else []):
try:
p, err = __import__(plugin), ''
except Exception, err:
p = None
err = str(err)
plugins[plugin] = (p, err)
if iswindows:
winutil, winutilerror = plugins['winutil']
if not winutil:
raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror)
sys.argv[1:] = winutil.argv()[1:]
win32event = __import__('win32event')
winerror = __import__('winerror')
win32api = __import__('win32api')
else:
import fcntl
_abspath = os.path.abspath
def my_abspath(path, encoding=sys.getfilesystemencoding()):
'''
Work around for buggy os.path.abspath. This function accepts either byte strings,
in which it calls os.path.abspath, or unicode string, in which case it first converts
to byte strings using `encoding`, calls abspath and then decodes back to unicode.
'''
to_unicode = False
if isinstance(path, unicode):
path = path.encode(encoding)
to_unicode = True
res = _abspath(path)
if to_unicode:
res = res.decode(encoding)
return res
os.path.abspath = my_abspath
_join = os.path.join
def my_join(a, *p):
encoding=sys.getfilesystemencoding()
p = [a] + list(p)
_unicode = False
for i in p:
if isinstance(i, unicode):
_unicode = True
break
p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p]
res = _join(*p)
if _unicode:
res = res.decode(encoding)
return res
os.path.join = my_join
def unicode_path(path, abs=False):
if not isinstance(path, unicode):
path = path.decode(sys.getfilesystemencoding())
if abs:
path = os.path.abspath(path)
return path
def osx_version():
if isosx:
@ -127,10 +33,6 @@ def osx_version():
return int(m.group(1)), int(m.group(2)), int(m.group(3))
# Default translation is NOOP
import __builtin__
__builtin__.__dict__['_'] = lambda s: s
class CommandLineError(Exception):
pass
@ -172,121 +74,6 @@ def setup_cli_handlers(logger, level):
logger.addHandler(handler)
class CustomHelpFormatter(IndentedHelpFormatter):
def format_usage(self, usage):
return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage)
def format_heading(self, heading):
return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE,
"", heading, terminal_controller.NORMAL)
def format_option(self, option):
result = []
opts = self.option_strings[option]
opt_width = self.help_position - self.current_indent - 2
if len(opts) > opt_width:
opts = "%*s%s\n" % (self.current_indent, "",
terminal_controller.GREEN+opts+terminal_controller.NORMAL)
indent_first = self.help_position
else: # start help on same line as opts
opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL),
terminal_controller.GREEN + opts + terminal_controller.NORMAL)
indent_first = 0
result.append(opts)
if option.help:
help_text = self.expand_default(option).split('\n')
help_lines = []
for line in help_text:
help_lines.extend(textwrap.wrap(line, self.help_width))
result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
result.extend(["%*s%s\n" % (self.help_position, "", line)
for line in help_lines[1:]])
elif opts[-1] != "\n":
result.append("\n")
return "".join(result)+'\n'
class OptionParser(_OptionParser):
def __init__(self,
usage='%prog [options] filename',
version='%%prog (%s %s)'%(__appname__, __version__),
epilog=_('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL,
gui_mode=False,
conflict_handler='resolve',
**kwds):
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
'''enclose the arguments in quotation marks.'''
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
formatter=CustomHelpFormatter(),
conflict_handler=conflict_handler, **kwds)
self.gui_mode = gui_mode
def error(self, msg):
if self.gui_mode:
raise Exception(msg)
_OptionParser.error(self, msg)
def merge(self, parser):
'''
Add options from parser to self. In case of conflicts, confilicting options from
parser are skipped.
'''
opts = list(parser.option_list)
groups = list(parser.option_groups)
def merge_options(options, container):
for opt in copy.deepcopy(options):
if not self.has_option(opt.get_opt_string()):
container.add_option(opt)
merge_options(opts, self)
for group in groups:
g = self.add_option_group(group.title)
merge_options(group.option_list, g)
def subsume(self, group_name, msg=''):
'''
Move all existing options into a subgroup named
C{group_name} with description C{msg}.
'''
opts = [opt for opt in self.options_iter() if opt.get_opt_string() not in ('--version', '--help')]
self.option_groups = []
subgroup = self.add_option_group(group_name, msg)
for opt in opts:
self.remove_option(opt.get_opt_string())
subgroup.add_option(opt)
def options_iter(self):
for opt in self.option_list:
if str(opt).strip():
yield opt
for gr in self.option_groups:
for opt in gr.option_list:
if str(opt).strip():
yield opt
def option_by_dest(self, dest):
for opt in self.options_iter():
if opt.dest == dest:
return opt
def merge_options(self, lower, upper):
'''
Merge options in lower and upper option lists into upper.
Default values in upper are overriden by
non default values in lower.
'''
for dest in lower.__dict__.keys():
if not upper.__dict__.has_key(dest):
continue
opt = self.option_by_dest(dest)
if lower.__dict__[dest] != opt.default and \
upper.__dict__[dest] == opt.default:
upper.__dict__[dest] = lower.__dict__[dest]
def load_library(name, cdll):
if iswindows:
@ -319,36 +106,37 @@ def extract(path, dir):
extractor(path, dir)
def get_proxies():
proxies = {}
if iswindows:
try:
winreg = __import__('_winreg')
settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows'
'\\CurrentVersion\\Internet Settings')
proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
if proxy:
server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
if ';' in server:
for p in server.split(';'):
protocol, address = p.split('=')
proxies[protocol] = address
else:
proxies['http'] = server
proxies['ftp'] = server
settings.Close()
except Exception, e:
print('Unable to detect proxy settings: %s' % str(e))
if proxies:
print('Using proxies: %s' % proxies)
else:
for q in ('http', 'ftp'):
proxy = os.environ.get(q+'_proxy', None)
if not proxy: continue
if proxy.startswith(q+'://'):
proxy = proxy[7:]
proxies[q] = proxy
return proxies
proxies = {}
for q in ('http', 'ftp'):
proxy = os.environ.get(q+'_proxy', None)
if not proxy: continue
if proxy.startswith(q+'://'):
proxy = proxy[7:]
proxies[q] = proxy
if iswindows:
try:
winreg = __import__('_winreg')
settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows'
'\\CurrentVersion\\Internet Settings')
proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
if proxy:
server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
if ';' in server:
for p in server.split(';'):
protocol, address = p.split('=')
proxies[protocol] = address
else:
proxies['http'] = server
proxies['ftp'] = server
settings.Close()
except Exception, e:
print('Unable to detect proxy settings: %s' % str(e))
if proxies:
print('Using proxies: %s' % proxies)
return proxies
def browser(honor_time=False):
@ -383,40 +171,6 @@ def fit_image(width, height, pwidth, pheight):
return scaled, int(width), int(height)
def get_lang():
lang = locale.getdefaultlocale()[0]
if lang is None and os.environ.has_key('LANG'): # Needed for OS X
try:
lang = os.environ['LANG']
except:
pass
if lang:
match = re.match('[a-z]{2,3}', lang)
if match:
lang = match.group()
return lang
def set_translator():
# To test different translations invoke as
# LC_ALL=de_DE.utf8 program
try:
from calibre.translations.compiled import translations
except:
return
lang = get_lang()
if lang:
buf = None
if os.access(lang+'.po', os.R_OK):
buf = cStringIO.StringIO()
make(lang+'.po', buf)
buf = cStringIO.StringIO(buf.getvalue())
elif translations.has_key(lang):
buf = cStringIO.StringIO(translations[lang])
if buf is not None:
t = GNUTranslations(buf)
t.install(unicode=True)
set_translator()
def sanitize_file_name(name):
'''
@ -501,120 +255,6 @@ def relpath(target, base=os.curdir):
rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:]
return os.path.join(*rel_list)
def _clean_lock_file(file):
try:
file.close()
except:
pass
try:
os.remove(file.name)
except:
pass
class LockError(Exception):
pass
class ExclusiveFile(object):
def __init__(self, path, timeout=10):
self.path = path
self.timeout = timeout
def __enter__(self):
self.file = open(self.path, 'a+b')
self.file.seek(0)
timeout = self.timeout
if iswindows:
name = ('Local\\'+(__appname__+self.file.name).replace('\\', '_'))[:201]
while self.timeout < 0 or timeout >= 0:
self.mutex = win32event.CreateMutex(None, False, name)
if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: break
time.sleep(1)
timeout -= 1
else:
while self.timeout < 0 or timeout >= 0:
try:
fcntl.lockf(self.file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
break
except IOError:
time.sleep(1)
timeout -= 1
if timeout < 0 and self.timeout >= 0:
self.file.close()
raise LockError
return self.file
def __exit__(self, type, value, traceback):
self.file.close()
if iswindows:
win32api.CloseHandle(self.mutex)
def singleinstance(name):
'''
Return True if no other instance of the application identified by name is running,
False otherwise.
@param name: The name to lock.
@type name: string
'''
if iswindows:
mutexname = 'mutexforsingleinstanceof'+__appname__+name
mutex = win32event.CreateMutex(None, False, mutexname)
if mutex:
atexit.register(win32api.CloseHandle, mutex)
return not win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS
else:
global _lock_file
path = os.path.expanduser('~/.'+__appname__+'_'+name+'.lock')
try:
f = open(path, 'w')
fcntl.lockf(f.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
atexit.register(_clean_lock_file, f)
return True
except IOError:
return False
return False
class Settings(QSettings):
def __init__(self, name='calibre2'):
QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope,
'kovidgoyal.net', name)
def get(self, key, default=None):
try:
key = str(key)
if not self.contains(key):
return default
val = str(self.value(key, QVariant()).toByteArray())
if not val:
return None
return cPickle.loads(val)
except:
return default
def set(self, key, val):
val = cPickle.dumps(val, -1)
self.setValue(str(key), QVariant(QByteArray(val)))
_settings = Settings()
if not _settings.get('rationalized'):
__settings = Settings(name='calibre')
dbpath = os.path.join(os.path.expanduser('~'), 'library1.db').decode(sys.getfilesystemencoding())
dbpath = unicode(__settings.value('database path',
QVariant(QString.fromUtf8(dbpath.encode('utf-8')))).toString())
cmdline = __settings.value('LRF conversion defaults', QVariant(QByteArray(''))).toByteArray().data()
_settings.set('database path', dbpath)
if cmdline:
cmdline = cPickle.loads(cmdline)
_settings.set('LRF conversion defaults', cmdline)
_settings.set('rationalized', True)
try:
os.unlink(unicode(__settings.fileName()))
except:
pass
_settings.set('database path', dbpath)
_spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
def english_sort(x, y):
'''
@ -662,10 +302,7 @@ def strftime(fmt, t=time.localtime()):
A version of strtime that returns unicode strings.
'''
result = time.strftime(fmt, t)
try:
return unicode(result, locale.getpreferredencoding(), 'replace')
except:
return unicode(result, 'utf-8', 'replace')
return unicode(result, preferred_encoding, 'replace')
def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
'''
@ -707,5 +344,10 @@ if isosx:
if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')):
from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts
for font in fonts:
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data'
open(os.path.join(fdir, font+'.ttf'), 'wb').write(font_data)
l = {}
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' in l
open(os.path.join(fdir, font+'.ttf'), 'wb').write(l['font_data'])
# Migrate from QSettings based config system
from calibre.utils.config import migrate
migrate()

30
src/calibre/constants.py Normal file
View File

@ -0,0 +1,30 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.4.83'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.
'''
import sys, locale, codecs
from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx)
isfrozen = hasattr(sys, 'frozen')
try:
preferred_encoding = locale.getpreferredencoding()
codecs.lookup(preferred_encoding)
except:
preferred_encoding = 'utf-8'
win32event = __import__('win32event') if iswindows else None
winerror = __import__('winerror') if iswindows else None
win32api = __import__('win32api') if iswindows else None
fcntl = None if iswindows else __import__('fcntl')

View File

@ -7,7 +7,8 @@ Embedded console for debugging.
'''
import sys, os, re
from calibre import OptionParser, iswindows
from calibre.utils.config import OptionParser
from calibre.constants import iswindows, isosx
from calibre.libunzip import update
def option_parser():
@ -25,7 +26,7 @@ Run an embedded python interpreter.
def update_zipfile(zipfile, mod, path):
if 'win32' in sys.platform:
print 'WARNING: On Windows Vista you must run this from a console that has been started in Administrator mode.'
print 'Press Enter to continue or Ctrl-C to Cancel'
print 'Press Enter to continue if this is an Administrator console or Ctrl-C to Cancel'
raw_input()
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1]
@ -35,8 +36,13 @@ def update_zipfile(zipfile, mod, path):
def update_module(mod, path):
if not hasattr(sys, 'frozen'):
raise RuntimeError('Modules can only be updated in frozen installs.')
if True or iswindows:
zp = None
if iswindows:
zp = os.path.join(os.path.dirname(sys.executable), 'library.zip')
elif isosx:
zp = os.path.join(os.path.dirname(getattr(sys, 'frameworks_dir')),
'Resources', 'lib', 'python2.5', 'site-packages.zip')
if zp is not None:
update_zipfile(zp, mod, path)
else:
raise ValueError('Updating modules is not supported on this platform.')

View File

@ -11,7 +11,7 @@ from optparse import OptionParser
from calibre import __version__, iswindows, __appname__
from calibre.devices.errors import PathError
from calibre.terminfo import TerminalController
from calibre.utils.terminfo import TerminalController
from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked
from calibre.devices import devices
from calibre.devices.scanner import DeviceScanner

View File

@ -135,7 +135,7 @@ class PRS505(Device):
if 'PRS-505/UC&' in device_id:
main = volumes[device_id]+':\\'
if not main:
DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__)
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__)
self._main_prefix = main
card = self._card_prefix = None
win32api = __import__('win32api')

View File

@ -0,0 +1,8 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Conversion to EPUB.
'''

View File

@ -0,0 +1,199 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Recursively parse HTML files to find all linked files. See :function:`traverse`.
'''
import sys, os, re
from urlparse import urlparse
from urllib import unquote
from calibre import unicode_path
from calibre.ebooks.chardet import xml_to_unicode
class Link(object):
'''
Represents a link in a HTML file.
'''
@classmethod
def url_to_local_path(cls, url, base):
path = url.path
if os.path.isabs(path):
return path
return os.path.abspath(os.path.join(base, path))
def __init__(self, url, base):
'''
:param url: The url this link points to. Must be an unquoted unicode string.
:param base: The base directory that relative URLs are with respect to.
Must be a unicode string.
'''
assert isinstance(url, unicode) and isinstance(base, unicode)
self.url = url
self.parsed_url = urlparse(unquote(self.url))
self.is_local = self.parsed_url.scheme in ('', 'file')
self.is_internal = self.is_local and not bool(self.parsed_url.path)
self.path = None
self.fragment = self.parsed_url.fragment
if self.is_local and not self.is_internal:
self.path = self.url_to_local_path(self.parsed_url, base)
def __hash__(self):
if self.path is None:
return hash(self.url)
return hash(self.path)
def __eq__(self, other):
return self.path == getattr(other, 'path', other)
def __str__(self):
return u'Link: %s --> %s'%(self.url, self.path)
class IgnoreFile(Exception):
def __init__(self, msg, errno):
Exception.__init__(self, msg)
self.doesnt_exist = errno == 2
self.errno = errno
class HTMLFile(object):
'''
Contains basic information about an HTML file. This
includes a list of links to other files as well as
the encoding of each file. Also tries to detect if the file is not a HTML
file in which case :member:`is_binary` is set to True.
The encoding of the file is available as :member:`encoding`.
'''
HTML_PAT = re.compile(r'<\s*html', re.IGNORECASE)
LINK_PAT = re.compile(
r'<\s*a\s+.*?href\s*=\s*(?:(?:"(?P<url1>[^"]+)")|(?:\'(?P<url2>[^\']+)\')|(?P<url3>[^\s]+))',
re.DOTALL|re.IGNORECASE)
def __init__(self, path_to_html_file, level, encoding, verbose):
'''
:param level: The level of this file. Should be 0 for the root file.
:param encoding: Use `encoding` to decode HTML.
'''
self.path = unicode_path(path_to_html_file, abs=True)
self.base = os.path.dirname(self.path)
self.level = level
self.links = []
try:
with open(self.path, 'rb') as f:
src = f.read()
except IOError, err:
msg = 'Could not read from file: %s with error: %s'%(self.path, unicode(err))
if level == 0:
raise IOError(msg)
raise IgnoreFile(msg, err.errno)
self.is_binary = not bool(self.HTML_PAT.search(src[:1024]))
if not self.is_binary:
if encoding is None:
encoding = xml_to_unicode(src[:4096], verbose=verbose)[-1]
self.encoding = encoding
src = src.decode(encoding, 'replace')
self.find_links(src)
def __eq__(self, other):
return self.path == getattr(other, 'path', other)
def __str__(self):
return u'HTMLFile:%d:%s:%s'%(self.level, 'b' if self.is_binary else 'a', self.path)
def __repr__(self):
return str(self)
def find_links(self, src):
for match in self.LINK_PAT.finditer(src):
url = None
for i in ('url1', 'url2', 'url3'):
url = match.group(i)
if url:
break
link = Link(url, self.base)
if link not in self.links:
self.links.append(link)
def depth_first(root, flat, visited=set([])):
yield root
visited.add(root)
for link in root.links:
if link.path is not None and link not in visited:
try:
index = flat.index(link)
except ValueError: # Can happen if max_levels is used
continue
hf = flat[index]
if hf not in visited:
yield hf
visited.add(hf)
for hf in depth_first(hf, flat, visited):
if hf not in visited:
yield hf
visited.add(hf)
def traverse(path_to_html_file, max_levels=sys.maxint, verbose=0, encoding=None):
'''
Recursively traverse all links in the HTML file.
:param max_levels: Maximum levels of recursion. Must be non-negative. 0
implies that no links in hte root HTML file are followed.
:param encoding: Specify character encoding of HTML files. If `None` it is
auto-detected.
:return: A pair of lists (breadth_first, depth_first). Each list contains
:class:`HTMLFile` objects.
'''
assert max_levels >= 0
level = 0
flat = [HTMLFile(path_to_html_file, level, encoding, verbose)]
next_level = list(flat)
while level < max_levels and len(next_level) > 0:
level += 1
nl = []
for hf in next_level:
rejects = []
for link in hf.links:
if link.path is None or link.path in flat:
continue
try:
nf = HTMLFile(link.path, level, encoding, verbose)
nl.append(nf)
flat.append(nf)
except IgnoreFile, err:
rejects.append(link)
if not err.doesnt_exist or verbose > 1:
print str(err)
for link in rejects:
hf.links.remove(link)
next_level = list(nl)
return flat, list(depth_first(flat[0], flat))
if __name__ == '__main__':
breadth_first, depth_first = traverse(sys.argv[1], verbose=2)
print 'Breadth first...'
for f in breadth_first: print f
print '\n\nDepth first...'
for f in depth_first: print f

View File

@ -10,6 +10,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net> ' \
import sys, struct, cStringIO, os
import functools
import re
from lxml import etree
from calibre.ebooks.lit import LitError
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
import calibre.ebooks.lit.mssha1 as mssha1
@ -17,6 +18,8 @@ from calibre import plugins
lzx, lxzerror = plugins['lzx']
msdes, msdeserror = plugins['msdes']
XML_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
"""
OPF_DECL = """<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE package
PUBLIC "+//ISBN 0-9673008-1-9//DTD OEB 1.0.1 Package//EN"
@ -107,11 +110,12 @@ class UnBinary(object):
AMPERSAND_RE = re.compile(
r'&(?!(?:#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z_:][a-zA-Z0-9.-_:]+);)')
def __init__(self, bin, manifest, map=OPF_MAP):
def __init__(self, bin, path, manifest, map=OPF_MAP):
self.manifest = manifest
self.tag_map, self.attr_map, self.tag_to_attr_map = map
self.opf = map is OPF_MAP
self.bin = bin
self.dir = os.path.dirname(path)
self.buf = cStringIO.StringIO()
self.binary_to_text()
self.raw = self.buf.getvalue().lstrip().decode('utf-8')
@ -122,9 +126,19 @@ class UnBinary(object):
def item_path(self, internal_id):
try:
return self.manifest[internal_id].path
target = self.manifest[internal_id].path
except KeyError:
return internal_id
if not self.dir:
return target
target = target.split('/')
base = self.dir.split('/')
for index in xrange(min(len(base), len(target))):
if base[index] != target[index]: break
else:
index += 1
relpath = (['..'] * (len(base) - index)) + target[index:]
return '/'.join(relpath)
def __unicode__(self):
return self.raw
@ -147,7 +161,7 @@ class UnBinary(object):
continue
elif c == '\v':
c = '\n'
self.buf.write(c.encode('utf-8'))
self.buf.write(c.encode('ascii', 'xmlcharrefreplace'))
elif state == 'get flags':
if oc == 0:
@ -206,7 +220,7 @@ class UnBinary(object):
state = 'get attr length'
continue
attr = None
if oc in current_map and current_map[oc]:
if current_map and oc in current_map and current_map[oc]:
attr = current_map[oc]
elif oc in self.attr_map:
attr = self.attr_map[oc]
@ -247,7 +261,8 @@ class UnBinary(object):
state = 'get attr'
elif count > 0:
if not in_censorship:
self.buf.write(unicode(c).encode('utf-8'))
self.buf.write(c.encode(
'ascii', 'xmlcharrefreplace'))
count -= 1
if count == 0:
if not in_censorship:
@ -299,7 +314,8 @@ class UnBinary(object):
path = self.item_path(doc)
if m and frag:
path += m + frag
self.buf.write((u'"%s"' % path).encode('utf-8'))
self.buf.write((u'"%s"' % path).encode(
'ascii', 'xmlcharrefreplace'))
state = 'get attr'
return index
@ -354,6 +370,8 @@ def preserve(function):
class LitReader(object):
PIECE_SIZE = 16
XML_PARSER = etree.XMLParser(
remove_blank_text=True, resolve_entities=False)
def magic():
@preserve
@ -596,16 +614,23 @@ class LitReader(object):
if item.path[0] == '/':
item.path = os.path.basename(item.path)
def _pretty_print(self, xml):
f = cStringIO.StringIO(xml.encode('utf-8'))
doc = etree.parse(f, parser=self.XML_PARSER)
pretty = etree.tostring(doc, encoding='ascii', pretty_print=True)
return XML_DECL + unicode(pretty)
def _read_meta(self):
path = 'content.opf'
raw = self.get_file('/meta')
try:
xml = OPF_DECL + unicode(UnBinary(raw, self.manifest, OPF_MAP))
xml = OPF_DECL + unicode(UnBinary(raw, path, self.manifest, OPF_MAP))
except LitError:
if 'PENGUIN group' not in raw: raise
print "WARNING: attempting PENGUIN malformed OPF fix"
raw = raw.replace(
'PENGUIN group', '\x00\x01\x18\x00PENGUIN group', 1)
xml = OPF_DECL + unicode(UnBinary(raw, self.manifest, OPF_MAP))
xml = OPF_DECL + unicode(UnBinary(raw, path, self.manifest, OPF_MAP))
self.meta = xml
def _read_drm(self):
@ -645,13 +670,6 @@ class LitReader(object):
key[i % 8] ^= ord(digest[i])
return ''.join(chr(x) for x in key)
def get_markup_file(self, name):
raw = self.get_file(name)
decl, map = (OPF_DECL, OPF_MAP) \
if name == '/meta' else (HTML_DECL, HTML_MAP)
xml = decl + unicode(UnBinary(raw, self.manifest, map))
return xml
def get_file(self, name):
entry = self.entries[name]
if entry.section == 0:
@ -748,7 +766,23 @@ class LitReader(object):
raise LitError("Failed to completely decompress section")
return ''.join(result)
def extract_content(self, output_dir=os.getcwdu()):
def get_entry_content(self, entry, pretty_print=False):
if 'spine' in entry.state:
name = '/'.join(('/data', entry.internal, 'content'))
path = entry.path
raw = self.get_file(name)
decl, map = (OPF_DECL, OPF_MAP) \
if name == '/meta' else (HTML_DECL, HTML_MAP)
content = decl + unicode(UnBinary(raw, path, self.manifest, map))
if pretty_print:
content = self._pretty_print(content)
content = content.encode('utf-8')
else:
name = '/'.join(('/data', entry.internal))
content = self.get_file(name)
return content
def extract_content(self, output_dir=os.getcwdu(), pretty_print=False):
output_dir = os.path.abspath(output_dir)
try:
opf_path = os.path.splitext(
@ -758,17 +792,15 @@ class LitReader(object):
opf_path = os.path.join(output_dir, opf_path)
self._ensure_dir(opf_path)
with open(opf_path, 'wb') as f:
f.write(self.meta.encode('utf-8'))
xml = self.meta
if pretty_print:
xml = self._pretty_print(xml)
f.write(xml.encode('utf-8'))
for entry in self.manifest.values():
path = os.path.join(output_dir, entry.path)
self._ensure_dir(path)
with open(path, 'wb') as f:
if 'spine' in entry.state:
name = '/'.join(('/data', entry.internal, 'content'))
f.write(self.get_markup_file(name).encode('utf-8'))
else:
name = '/'.join(('/data', entry.internal))
f.write(self.get_file(name))
f.write(self.get_entry_content(entry, pretty_print))
def _ensure_dir(self, path):
dir = os.path.dirname(path)
@ -776,11 +808,14 @@ class LitReader(object):
os.makedirs(dir)
def option_parser():
from calibre import OptionParser
from calibre.utils.config import OptionParser
parser = OptionParser(usage=_('%prog [options] LITFILE'))
parser.add_option(
'-o', '--output-dir', default='.',
help=_('Output directory. Defaults to current directory.'))
parser.add_option(
'-p', '--pretty-print', default=False, action='store_true',
help=_('Legibly format extracted markup. May modify meaningful whitespace.'))
parser.add_option(
'--verbose', default=False, action='store_true',
help=_('Useful for debugging.'))
@ -793,7 +828,7 @@ def main(args=sys.argv):
parser.print_help()
return 1
lr = LitReader(args[1])
lr.extract_content(opts.output_dir)
lr.extract_content(opts.output_dir, opts.pretty_print)
print _('OEB ebook created in'), opts.output_dir
return 0

View File

@ -14,7 +14,8 @@ from calibre.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \
Paragraph, TextStyle, BlockStyle
from calibre.ebooks.lrf.fonts import FONT_FILE_MAP
from calibre.ebooks import ConversionError
from calibre import __appname__, __version__, __author__, iswindows, OptionParser
from calibre import __appname__, __version__, __author__, iswindows
from calibre.utils.config import OptionParser
__docformat__ = "epytext"

View File

@ -121,7 +121,7 @@ class PageProcessor(list):
DestroyMagickWand(img)
MagickCropImage(split1, (width/2)-1, height, 0, 0)
MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split1, split2]
self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages()
except Exception, err:
@ -180,7 +180,7 @@ class PageProcessor(list):
MagickSetImageType(wand, GrayscaleType)
MagickQuantizeImage(wand, self.opts.colors, RGBColorspace, 0, 1, 0)
dest = '%d_%d%s'%(self.num, i, os.path.splitext(self.path_to_page)[-1])
dest = '%d_%d.png'%(self.num, i)
dest = os.path.join(self.dest, dest)
MagickWriteImage(wand, dest)
self.append(dest)
@ -219,7 +219,7 @@ def process_pages(pages, opts, update):
for pp in processed_pages:
if len(pp) == 0:
failures.append(os.path.basename(pp.path_to_page()))
failures.append(os.path.basename(pp.path_to_page))
else:
ans += pp
return ans, failures, tdir
@ -238,7 +238,7 @@ def config(defaults=None):
c.add_opt('output', ['-o', '--output'],
help=_('Path to output LRF file. By default a file is created in the current directory.'))
c.add_opt('colors', ['-c', '--colors'], type='int', default=64,
help=_('Number of colors for Grayscale image conversion. Default: %default'))
help=_('Number of colors for grayscale image conversion. Default: %default'))
c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
@ -247,6 +247,8 @@ def config(defaults=None):
help=_('Disable sharpening.'))
c.add_opt('landscape', ['-l', '--landscape'], default=False,
help=_("Don't split landscape images into two portrait images"))
c.add_opt('right2left', ['--right2left'], default=False, action='store_true',
help=_('Used for right-to-left publications like manga. Causes landscape pages to be split into portrait pages from right to left.'))
c.add_opt('no_sort', ['--no-sort'], default=False,
help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),

View File

@ -8,7 +8,7 @@ from calibre.ebooks import ConversionError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPF
from calibre.ebooks.metadata.epub import OCFDirReader
from calibre.libunzip import extract as zip_extract
from calibre.utils.zipfile import ZipFile
from calibre import __appname__, setup_cli_handlers
@ -26,7 +26,7 @@ def generate_html(pathtoepub, logger):
tdir = mkdtemp(prefix=__appname__+'_')
os.rmdir(tdir)
try:
zip_extract(pathtoepub, tdir)
ZipFile(pathtoepub).extractall(tdir)
except:
if os.path.exists(tdir) and os.path.isdir(tdir):
shutil.rmtree(tdir)

View File

@ -2,7 +2,8 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, logging, os
from calibre import setup_cli_handlers, OptionParser
from calibre import setup_cli_handlers
from calibre.utils.config import OptionParser
from calibre.ebooks import ConversionError
from calibre.ebooks.lrf.meta import get_metadata
from calibre.ebooks.lrf.lrfparser import LRFDocument

View File

@ -10,6 +10,8 @@ import os, tempfile, atexit, shutil, time
from PyQt4.Qt import QWebPage, QUrl, QApplication, QSize, \
SIGNAL, QPainter, QImage, QObject, Qt
from calibre.parallel import ParallelJob
__app = None
class HTMLTableRenderer(QObject):
@ -80,18 +82,17 @@ def render_table(server, soup, table, css, base_dir, width, height, dpi, factor=
</body>
</html>
'''%(head, width-10, style, unicode(table))
server.run_job(1, 'render_table',
job = ParallelJob('render_table', lambda j : j, None,
args=[html, base_dir, width, height, dpi, factor])
res = None
while res is None:
server.add_job(job)
while not job.has_run:
time.sleep(2)
res = server.result(1)
result, exception, traceback = res
if exception:
if job.exception is not None:
print 'Failed to render table'
print exception
print traceback
images, tdir = result
print job.exception
print job.traceback
images, tdir = job.result
atexit.register(shutil.rmtree, tdir)
return images

View File

@ -3,19 +3,12 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, shutil, glob, logging
from tempfile import mkdtemp
from subprocess import Popen, PIPE
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.lit.reader import LitReader
from calibre.ebooks import ConversionError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPFReader
from calibre import isosx, __appname__, setup_cli_handlers, islinux
CLIT = 'clit'
if isosx and hasattr(sys, 'frameworks_dir'):
CLIT = os.path.join(getattr(sys, 'frameworks_dir'), CLIT)
if islinux and getattr(sys, 'frozen_path', False):
CLIT = os.path.join(getattr(sys, 'frozen_path'), 'clit')
from calibre import __appname__, setup_cli_handlers
def option_parser():
parser = lrf_option_parser(
@ -24,37 +17,15 @@ _('''Usage: %prog [options] mybook.lit
%prog converts mybook.lit to mybook.lrf''')
)
parser.add_option('--lit2oeb', default=False, dest='lit2oeb', action='store_true',
help='Use the new lit2oeb to convert lit files instead of convertlit.')
return parser
def generate_html2(pathtolit, logger):
if not os.access(pathtolit, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtolit
tdir = mkdtemp(prefix=__appname__+'_')
lr = LitReader(pathtolit)
print 'Extracting LIT file to', tdir
lr.extract_content(tdir)
return tdir
def generate_html(pathtolit, logger):
if not os.access(pathtolit, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtolit
tdir = mkdtemp(prefix=__appname__+'_')
os.rmdir(tdir)
cmd = [CLIT, pathtolit, '%s'%(tdir+os.sep)]
logger.debug(repr(cmd))
p = Popen(cmd, stderr=PIPE, stdout=PIPE)
stdout = p.stdout.read()
err = p.stderr.read()
logger.info(p.stdout.read())
ret = p.wait()
if ret != 0:
if os.path.exists(tdir) and os.path.isdir(tdir):
shutil.rmtree(tdir)
if 'keys.txt' in unicode(err)+unicode(stdout):
raise ConversionError('This lit file is protected by DRM. You must first use the ConvertLIT program to remove the DRM. Doing so may be illegal, and so %s does not do this, nor does it provide instructions on how to do it.'%(__appname__,))
raise ConversionError, err
tdir = mkdtemp(prefix=__appname__+'_'+'lit2oeb_')
lr = LitReader(pathtolit)
print 'Extracting LIT file to', tdir
lr.extract_content(tdir)
return tdir
def process_file(path, options, logger=None):
@ -63,8 +34,7 @@ def process_file(path, options, logger=None):
logger = logging.getLogger('lit2lrf')
setup_cli_handlers(logger, level)
lit = os.path.abspath(os.path.expanduser(path))
tdir = generate_html2(lit, logger) if getattr(options, 'lit2oeb', False) \
else generate_html(lit, logger)
tdir = generate_html(lit, logger)
try:
opf = glob.glob(os.path.join(tdir, '*.opf'))
if opf:
@ -96,7 +66,6 @@ def process_file(path, options, logger=None):
options.output = os.path.abspath(os.path.basename(os.path.splitext(path)[0]) + ext)
options.output = os.path.abspath(os.path.expanduser(options.output))
options.use_spine = True
html_process_file(htmlfile, options, logger=logger)
finally:
try:

View File

@ -4,7 +4,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, array, os, re, codecs, logging
from calibre import OptionParser, setup_cli_handlers
from calibre import setup_cli_handlers
from calibre.utils.config import OptionParser
from calibre.ebooks.lrf.meta import LRFMetaFile
from calibre.ebooks.lrf.objects import get_object, PageTree, StyleObject, \
Font, Text, TOCObject, BookAttr, ruby_tags

View File

@ -6,7 +6,8 @@ Compile a LRS file into a LRF file.
import sys, os, logging
from calibre import OptionParser, setup_cli_handlers
from calibre import setup_cli_handlers
from calibre.utils.config import OptionParser
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, \
CData, Tag
from calibre.ebooks.lrf.pylrs.pylrs import Book, PageStyle, TextStyle, \

View File

@ -574,8 +574,8 @@ class LRFMetaFile(object):
def option_parser():
from optparse import OptionParser
from calibre import __appname__, __version__
from calibre.utils.config import OptionParser
from calibre.constants import __appname__, __version__
parser = OptionParser(usage = \
_('''%prog [options] mybook.lrf

View File

@ -25,6 +25,8 @@ def generate_html(pathtopdf, logger):
Convert the pdf into html.
@return: Path to a temporary file containing the HTML.
'''
if isinstance(pathtopdf, unicode):
pathtopdf = pathtopdf.encode(sys.getfilesystemencoding())
if not os.access(pathtopdf, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtopdf
tdir = PersistentTemporaryDirectory('pdftohtml')

View File

@ -7,7 +7,8 @@ Convert PDF to a reflowable format using pdftoxml.exe as the PDF parsing backend
import sys, os, re, tempfile, subprocess, atexit, shutil, logging, xml.parsers.expat
from xml.etree.ElementTree import parse
from calibre import isosx, OptionParser, setup_cli_handlers, __appname__
from calibre import isosx, setup_cli_handlers, __appname__
from calibre.utils.config import OptionParser
from calibre.ebooks import ConversionError
PDFTOXML = 'pdftoxml.exe'

View File

@ -11,8 +11,9 @@ from urllib import unquote, quote
from urlparse import urlparse
from calibre import __version__ as VERSION, relpath
from calibre import OptionParser
from calibre.constants import __version__ as VERSION
from calibre import relpath
from calibre.utils.config import OptionParser
def get_parser(extension):
''' Return an option parser with the basic metadata options already setup'''

View File

@ -7,7 +7,8 @@ Interface to isbndb.com. My key HLLXQX2A.
import sys, logging, re, socket
from urllib import urlopen, quote
from calibre import setup_cli_handlers, OptionParser
from calibre import setup_cli_handlers
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup

View File

@ -6,7 +6,8 @@ Fetch cover from LibraryThing.com based on ISBN number.
import sys, socket, os, re, mechanize
from calibre import browser as _browser, OptionParser
from calibre import browser as _browser
from calibre.utils.config import OptionParser
from calibre.ebooks.BeautifulSoup import BeautifulSoup
browser = None

View File

@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, collections
from calibre.utils.config import prefs
from calibre.ebooks.metadata.rtf import get_metadata as rtf_metadata
from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata
from calibre.ebooks.lrf.meta import get_metadata as lrf_metadata
@ -94,20 +95,11 @@ def set_metadata(stream, mi, stream_type='lrf'):
elif stream_type == 'rtf':
set_rtf_metadata(stream, mi)
_filename_pat = re.compile(ur'(?P<title>.+) - (?P<author>[^_]+)')
def get_filename_pat():
return _filename_pat.pattern
def set_filename_pat(pat):
global _filename_pat
_filename_pat = re.compile(pat)
def metadata_from_filename(name, pat=None):
name = os.path.splitext(name)[0]
mi = MetaInformation(None, None)
if pat is None:
pat = _filename_pat
pat = re.compile(prefs.get('filename_pattern'))
match = pat.search(name)
if match:
try:
@ -134,12 +126,12 @@ def metadata_from_filename(name, pat=None):
try:
si = match.group('series_index')
mi.series_index = int(si)
except IndexError, ValueError:
except (IndexError, ValueError):
pass
try:
si = match.group('isbn')
mi.isbn = si
except IndexError, ValueError:
except (IndexError, ValueError):
pass
if not mi.title:
mi.title = name

View File

@ -93,7 +93,7 @@ class BookHeader(object):
1252 : 'cp1252',
65001 : 'utf-8',
}[self.codepage]
except IndexError, KeyError:
except (IndexError, KeyError):
print '[WARNING] Unknown codepage %d. Assuming cp-1252'%self.codepage
self.codec = 'cp1252'
@ -408,7 +408,7 @@ def get_metadata(stream):
return mi
def option_parser():
from calibre import OptionParser
from calibre.utils.config import OptionParser
parser = OptionParser(usage=_('%prog [options] myebook.mobi'))
parser.add_option('-o', '--output-dir', default='.',
help=_('Output directory. Defaults to current directory.'))

View File

@ -2,19 +2,49 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import sys, os, re, StringIO, traceback
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, \
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
from calibre import __author__, islinux, iswindows, Settings, isosx, get_lang
from calibre import __author__, islinux, iswindows, isosx
from calibre.startup import get_lang
from calibre.utils.config import Config, ConfigProxy, dynamic
import calibre.resources as resources
NONE = QVariant() #: Null value to return from the data function of item models
def _config():
c = Config('gui', 'preferences for the calibre GUI')
c.add_opt('frequently_used_directories', default=[],
help=_('Frequently used directories'))
c.add_opt('send_to_device_by_default', default=True,
help=_('Send downloaded periodical content to device automatically'))
c.add_opt('save_to_disk_single_format', default='lrf',
help=_('The format to use when saving single files to disk'))
c.add_opt('confirm_delete', default=False,
help=_('Confirm before deleting'))
c.add_opt('toolbar_icon_size', default=QSize(48, 48),
help=_('Toolbar icon size')) # value QVariant.toSize
c.add_opt('show_text_in_toolbar', default=True,
help=_('Show button labels in the toolbar'))
c.add_opt('main_window_geometry', default=None,
help=_('Main window geometry')) # value QVariant.toByteArray
c.add_opt('new_version_notification', default=True,
help=_('Notify when a new version is available'))
c.add_opt('use_roman_numerals_for_series_number', default=True,
help=_('Use Roman numerals for series number'))
c.add_opt('cover_flow_queue_length', default=6,
help=_('Number of covers to show in the cover browsing mode'))
c.add_opt('LRF_conversion_defaults', default=[],
help=_('Defaults for conversion to LRF'))
c.add_opt('LRF_ebook_viewer_options', default=None,
help=_('Options for the LRF ebook viewer'))
return ConfigProxy(c)
config = _config()
# Turn off DeprecationWarnings in windows GUI
if iswindows:
import warnings
@ -84,20 +114,33 @@ def human_readable(size):
size = size[:-2]
return size + " " + suffix
class Dispatcher(QObject):
'''Convenience class to ensure that a function call always happens in the GUI thread'''
def __init__(self, func):
QObject.__init__(self)
self.func = func
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'),
self.dispatch, Qt.QueuedConnection)
def __call__(self, *args, **kwargs):
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs)
def dispatch(self, args, kwargs):
self.func(*args, **kwargs)
class TableView(QTableView):
def __init__(self, parent):
QTableView.__init__(self, parent)
self.read_settings()
def read_settings(self):
self.cw = Settings().get(self.__class__.__name__ + ' column widths')
self.cw = dynamic[self.__class__.__name__+'column widths']
def write_settings(self):
settings = Settings()
settings.set(self.__class__.__name__ + ' column widths',
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))]))
dynamic[self.__class__.__name__+'column widths'] = \
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
def restore_column_widths(self):
if self.cw and len(self.cw):
@ -110,10 +153,11 @@ class TableView(QTableView):
@param cols: A list of booleans or None. If an entry is False the corresponding column
is hidden, if True it is shown.
'''
key = self.__class__.__name__+'visible columns'
if cols:
Settings().set(self.__class__.__name__ + ' visible columns', cols)
dynamic[key] = cols
else:
cols = Settings().get(self.__class__.__name__ + ' visible columns')
cols = dynamic[key]
if not cols:
cols = [True for i in range(self.model().columnCount(self))]
@ -217,7 +261,7 @@ _sidebar_directories = []
def set_sidebar_directories(dirs):
global _sidebar_directories
if dirs is None:
dirs = Settings().get('frequently used directories', [])
dirs = config['frequently_used_directories']
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
class FileDialog(QObject):
@ -240,10 +284,10 @@ class FileDialog(QObject):
if add_all_files_filter or not ftext:
ftext += 'All files (*)'
settings = Settings()
self.dialog_name = name if name else 'dialog_' + title
self.selected_files = None
self.fd = None
if islinux:
self.fd = QFileDialog(parent)
self.fd.setFileMode(mode)
@ -251,15 +295,15 @@ class FileDialog(QObject):
self.fd.setModal(modal)
self.fd.setFilter(ftext)
self.fd.setWindowTitle(title)
state = settings.get(self.dialog_name, QByteArray())
if not self.fd.restoreState(state):
state = dynamic[self.dialog_name]
if not state or not self.fd.restoreState(state):
self.fd.setDirectory(os.path.expanduser('~'))
osu = [i for i in self.fd.sidebarUrls()]
self.fd.setSidebarUrls(osu + _sidebar_directories)
QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir)
self.accepted = self.fd.exec_() == QFileDialog.Accepted
else:
dir = settings.get(self.dialog_name, os.path.expanduser('~'))
dir = dynamic.get(self.dialog_name, default=os.path.expanduser('~'))
self.selected_files = []
if mode == QFileDialog.AnyFile:
f = qstring_to_unicode(
@ -284,7 +328,7 @@ class FileDialog(QObject):
self.selected_files.append(f)
if self.selected_files:
self.selected_files = [qstring_to_unicode(q) for q in self.selected_files]
settings.set(self.dialog_name, os.path.dirname(self.selected_files[0]))
dynamic[self.dialog_name] = os.path.dirname(self.selected_files[0])
self.accepted = bool(self.selected_files)
@ -298,8 +342,7 @@ class FileDialog(QObject):
def save_dir(self):
if self.fd:
settings = Settings()
settings.set(self.dialog_name, self.fd.saveState())
dynamic[self.dialog_name] = self.fd.saveState()
def choose_dir(window, name, title):

View File

@ -12,7 +12,8 @@ import sys, os
from PyQt4.QtGui import QImage, QSizePolicy
from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject
from calibre import Settings, plugins
from calibre import plugins
from calibre.gui2 import config
pictureflow, pictureflowerror = plugins['pictureflow']
if pictureflow is not None:
@ -70,7 +71,7 @@ if pictureflow is not None:
def __init__(self, height=300, parent=None):
pictureflow.PictureFlow.__init__(self, parent,
Settings().get('cover flow queue length', 6)+1)
config['cover_flow_queue_length']+1)
self.setSlideSize(QSize(int(2/3. * height), height))
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25))
self.setFocusPolicy(Qt.WheelFocus)

View File

@ -1,131 +1,185 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, traceback
import os, traceback, Queue, time
from threading import Thread
from PyQt4.QtCore import QThread, SIGNAL, QObject
from calibre import iswindows
from calibre.devices import devices
from calibre.parallel import Job
from calibre.devices.scanner import DeviceScanner
class DeviceDetector(QThread):
class DeviceJob(Job):
def __init__(self, func, *args, **kwargs):
Job.__init__(self, *args, **kwargs)
self.func = func
def run(self):
self.start_work()
try:
self.result = self.func(*self.args, **self.kwargs)
except (Exception, SystemExit), err:
self.exception = err
self.traceback = traceback.format_exc()
finally:
self.job_done()
class DeviceManager(Thread):
'''
Worker thread that polls the USB ports for devices. Emits the
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
disconnection events.
'''
def __init__(self, sleep_time=2000):
def __init__(self, connected_slot, job_manager, sleep_time=2):
'''
@param sleep_time: Time to sleep between device probes in millisecs
@type sleep_time: integer
'''
self.devices = [[d, False] for d in devices()]
self.sleep_time = sleep_time
QThread.__init__(self)
self.keep_going = True
Thread.__init__(self)
self.setDaemon(True)
self.devices = [[d, False] for d in devices()]
self.device = None
self.device_class = None
self.sleep_time = sleep_time
self.connected_slot = connected_slot
self.jobs = Queue.Queue(0)
self.keep_going = True
self.job_manager = job_manager
self.current_job = None
self.scanner = DeviceScanner()
def detect_device(self):
self.scanner.scan()
for device in self.devices:
connected = self.scanner.is_device_connected(device[0])
if connected and not device[1]:
try:
dev = device[0]()
dev.open()
self.device = dev
self.device_class = dev.__class__
self.connected_slot(True)
except:
print 'Unable to open device'
traceback.print_exc()
finally:
device[1] = True
elif not connected and device[1]:
while True:
try:
job = self.jobs.get_nowait()
job.abort(Exception(_('Device no longer connected.')))
except Queue.Empty:
break
self.device = None
self.connected_slot(False)
device[1] ^= True
def next(self):
if not self.jobs.empty():
try:
return self.jobs.get_nowait()
except Queue.Empty:
pass
def run(self):
scanner = DeviceScanner()
while self.keep_going:
scanner.scan()
for device in self.devices:
connected = scanner.is_device_connected(device[0])
if connected and not device[1]:
try:
dev = device[0]()
dev.open()
self.emit(SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), dev, True)
except:
print 'Unable to open device'
traceback.print_exc()
finally:
device[1] = True
elif not connected and device[1]:
self.emit(SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), device[0], False)
device[1] ^= True
self.msleep(self.sleep_time)
self.detect_device()
while True:
job = self.next()
if job is not None:
self.current_job = job
self.device.set_progress_reporter(job.update_status)
self.current_job.run()
self.current_job = None
else:
break
time.sleep(self.sleep_time)
def create_job(self, func, done, description, args=[], kwargs={}):
job = DeviceJob(func, done, self.job_manager,
args=args, kwargs=kwargs, description=description)
self.job_manager.add_job(job)
self.jobs.put(job)
return job
def _get_device_information(self):
info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
cp = self.device.card_prefix(end_session=False)
fs = self.device.free_space()
return info, cp, fs
def get_device_information(self, done):
'''Get device information and free space on device'''
return self.create_job(self._get_device_information, done,
description=_('Get device information'))
def _books(self):
'''Get metadata from device'''
mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True)
return (mainlist, cardlist)
class DeviceManager(QObject):
def __init__(self, device):
QObject.__init__(self)
self.device_class = device.__class__
self.device = device
def device_removed(self):
self.device = None
def info_func(self):
''' Return callable that returns device information and free space on device'''
def get_device_information(updater):
'''Get device information'''
self.device.set_progress_reporter(updater)
info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
cp = self.device.card_prefix(end_session=False)
fs = self.device.free_space()
return info, cp, fs
return get_device_information
def books_func(self):
def books(self, done):
'''Return callable that returns the list of books on device as two booklists'''
def books(updater):
'''Get metadata from device'''
self.device.set_progress_reporter(updater)
mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True)
return (mainlist, cardlist)
return books
return self.create_job(self._books, done, description=_('Get list of books on device'))
def sync_booklists_func(self):
'''Upload booklists to device'''
def sync_booklists(updater, booklists):
'''Sync metadata to device'''
self.device.set_progress_reporter(updater)
self.device.sync_booklists(booklists, end_session=False)
return self.device.card_prefix(end_session=False), self.device.free_space()
return sync_booklists
def _sync_booklists(self, booklists):
'''Sync metadata to device'''
self.device.sync_booklists(booklists, end_session=False)
return self.device.card_prefix(end_session=False), self.device.free_space()
def upload_books_func(self):
'''Upload books to device'''
def upload_books(updater, files, names, on_card=False):
'''Upload books to device: '''
self.device.set_progress_reporter(updater)
return self.device.upload_books(files, names, on_card, end_session=False)
return upload_books
def sync_booklists(self, done, booklists):
return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device'))
def _upload_books(self, files, names, on_card=False):
'''Upload books to device: '''
return self.device.upload_books(files, names, on_card, end_session=False)
def upload_books(self, done, files, names, on_card=False, titles=None):
desc = _('Upload %d books to device')%len(names)
if titles:
desc += u':' + u', '.join(titles)
return self.create_job(self._upload_books, done, args=[files, names],
kwargs={'on_card':on_card}, description=desc)
def add_books_to_metadata(self, locations, metadata, booklists):
self.device_class.add_books_to_metadata(locations, metadata, booklists)
self.device.add_books_to_metadata(locations, metadata, booklists)
def delete_books_func(self):
def _delete_books(self, paths):
'''Remove books from device'''
def delete_books(updater, paths):
'''Remove books from device'''
self.device.delete_books(paths, end_session=True)
return delete_books
self.device.delete_books(paths, end_session=True)
def delete_books(self, done, paths):
return self.create_job(self._delete_books, done, args=[paths],
description=_('Delete books from device'))
def remove_books_from_metadata(self, paths, booklists):
self.device_class.remove_books_from_metadata(paths, booklists)
self.device.remove_books_from_metadata(paths, booklists)
def save_books_func(self):
def _save_books(self, paths, target):
'''Copy books from device to disk'''
def save_books(updater, paths, target):
'''Copy books from device to disk'''
self.device.set_progress_reporter(updater)
for path in paths:
name = path.rpartition('/')[2]
f = open(os.path.join(target, name), 'wb')
self.device.get_file(path, f)
f.close()
return save_books
def view_book_func(self):
'''Copy book from device to local hdd for viewing'''
def view_book(updater, path, target):
self.device.set_progress_reporter(updater)
f = open(target, 'wb')
for path in paths:
name = path.rpartition('/')[2]
f = open(os.path.join(target, name), 'wb')
self.device.get_file(path, f)
f.close()
return target
return view_book
def save_books(self, done, paths, target):
return self.create_job(self._save_books, done, args=[paths, target],
description=_('Download books from device'))
def _view_book(self, path, target):
f = open(target, 'wb')
self.device.get_file(path, f)
f.close()
return target
def view_book(self, done, path, target):
return self.create_job(self._view_book, done, args=[path, target],
description=_('View book on device'))

View File

@ -14,17 +14,13 @@ def set_conversion_defaults(window):
d.exec_()
def get_bulk_conversion_options(window):
c = config(None)
with open(c.config_file_path, 'rb') as f:
d = ComicConf(window, config_defaults=f.read())
d = ComicConf(window, config_defaults=config(None).as_string())
if d.exec_() == QDialog.Accepted:
return d.config.parse()
def get_conversion_options(window, defaults, title, author):
if defaults is None:
c = config(None)
with open(c.config_file_path, 'rb') as f:
defaults = f.read()
defaults = config(None).as_string()
defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author))
d = ComicConf(window, config_defaults=defaults, generic=False)
if d.exec_() == QDialog.Accepted:
@ -61,6 +57,7 @@ class ComicConf(QDialog, Ui_Dialog):
self.opt_dont_sharpen.setChecked(opts.dont_sharpen)
self.opt_landscape.setChecked(opts.landscape)
self.opt_no_sort.setChecked(opts.no_sort)
self.opt_right2left.setChecked(opts.right2left)
for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False)

View File

@ -107,14 +107,14 @@
</property>
</widget>
</item>
<item row="8" column="0" >
<item row="9" column="0" >
<widget class="QCheckBox" name="opt_no_sort" >
<property name="text" >
<string>Dont so&amp;rt</string>
<string>Don't so&amp;rt</string>
</property>
</widget>
</item>
<item row="9" column="1" >
<item row="10" column="1" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
@ -124,6 +124,13 @@
</property>
</widget>
</item>
<item row="8" column="0" >
<widget class="QCheckBox" name="opt_right2left" >
<property name="text" >
<string>&amp;Right to left</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>

View File

@ -5,14 +5,14 @@ import os
from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon
from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant
from calibre import islinux, Settings
from calibre import islinux
from calibre.gui2.dialogs.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config
from calibre.utils.config import prefs
from calibre.gui2.widgets import FilenamePattern
from calibre.ebooks import BOOK_EXTENSIONS
class ConfigDialog(QDialog, Ui_Dialog):
def __init__(self, window, db, columns):
@ -24,17 +24,16 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.item2 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.db = db
self.current_cols = columns
settings = Settings()
path = settings.get('library path')
path = prefs['library_path']
self.location.setText(path)
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
dirs = settings.get('frequently used directories', [])
rn = settings.get('use roman numerals for series number', True)
self.timeout.setValue(settings.get('network timeout', 5))
dirs = config['frequently_used_directories']
rn = config['use_roman_numerals_for_series_number']
self.timeout.setValue(prefs['network_timeout'])
self.roman_numerals.setChecked(rn)
self.new_version_notification.setChecked(settings.get('new version notification', True))
self.new_version_notification.setChecked(config['new_version_notification'])
self.directory_list.addItems(dirs)
self.connect(self.add_button, SIGNAL('clicked(bool)'), self.add_dir)
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_dir)
@ -56,17 +55,17 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
icons = settings.get('toolbar icon size', self.ICON_SIZES[0])
icons = config['toolbar_icon_size']
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
self.show_toolbar_text.setChecked(settings.get('show text in toolbar', True))
self.show_toolbar_text.setChecked(config['show_text_in_toolbar'])
for ext in BOOK_EXTENSIONS:
self.single_format.addItem(ext.upper(), QVariant(ext))
single_format = settings.get('save to disk single format', 'lrf')
single_format = config['save_to_disk_single_format']
self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
self.cover_browse.setValue(settings.get('cover flow queue length', 6))
self.confirm_delete.setChecked(settings.get('confirm delete', False))
self.cover_browse.setValue(config['cover_flow_queue_length'])
self.confirm_delete.setChecked(config['confirm_delete'])
def compact(self, toggled):
d = Vacuum(self, self.db)
@ -88,19 +87,18 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.directory_list.takeItem(idx)
def accept(self):
settings = Settings()
settings.set('use roman numerals for series number', bool(self.roman_numerals.isChecked()))
settings.set('new version notification', bool(self.new_version_notification.isChecked()))
settings.set('network timeout', int(self.timeout.value()))
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value())
path = qstring_to_unicode(self.location.text())
self.final_columns = [self.columns.item(i).checkState() == Qt.Checked for i in range(self.columns.count())]
settings.set('toolbar icon size', self.ICON_SIZES[self.toolbar_button_size.currentIndex()])
settings.set('show text in toolbar', bool(self.show_toolbar_text.isChecked()))
settings.set('confirm delete', bool(self.confirm_delete.isChecked()))
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['confirm_delete'] = bool(self.confirm_delete.isChecked())
pattern = self.filename_pattern.commit()
settings.set('filename pattern', pattern)
settings.set('save to disk single format', BOOK_EXTENSIONS[self.single_format.currentIndex()])
settings.set('cover flow queue length', self.cover_browse.value())
prefs['filename_pattern'] = pattern
config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()]
config['cover_flow_queue_length'] = self.cover_browse.value()
if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'),
@ -113,7 +111,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
else:
self.database_location = os.path.abspath(path)
self.directories = [qstring_to_unicode(self.directory_list.item(i).text()) for i in range(self.directory_list.count())]
settings.set('frequently used directories', self.directories)
config['frequently_used_directories'] = self.directories
QDialog.accept(self)
class Vacuum(QMessageBox):

View File

@ -13,7 +13,7 @@ from PyQt4.QtGui import QDialog, QItemSelectionModel
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog
from calibre.ebooks.metadata.isbndb import create_books, option_parser, ISBNDBError
from calibre import Settings
from calibre.utils.config import prefs
class Matches(QAbstractTableModel):
@ -76,7 +76,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.timeout = timeout
QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata)
self.key.setText(Settings().get('isbndb.com key', ''))
self.key.setText(prefs['isbndb_com_key'])
self.setWindowTitle(title if title else 'Unknown')
self.tlabel.setText(self.tlabel.text().arg(title if title else 'Unknown'))
@ -105,7 +105,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
_('You must specify a valid access key for isbndb.com'))
return
else:
Settings().set('isbndb.com key', key)
prefs['isbndb_com_key'] = key
args = ['isbndb']
if self.isbn:

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Display active jobs'''
from PyQt4.QtCore import Qt, QObject, SIGNAL, QSize, QString
from PyQt4.QtCore import Qt, QObject, SIGNAL, QSize, QString, QTimer
from PyQt4.QtGui import QDialog, QAbstractItemDelegate, QStyleOptionProgressBarV2, \
QApplication, QStyle
@ -45,10 +45,20 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.running_time_timer = QTimer(self)
self.connect(self.running_time_timer, SIGNAL('timeout()'), self.update_running_time)
self.running_time_timer.start(1000)
def update_running_time(self):
model = self.model
for row, job in enumerate(model.jobs):
if job.is_running:
self.jobs_view.dataChanged(model.index(row, 3), model.index(row, 3))
def kill_job(self):
for index in self.jobs_view.selectedIndexes():
row = index.row()
self.emit(SIGNAL('kill_job(int, PyQt_PyObject)'), row, self)
self.model.kill_job(row, self)
return
def closeEvent(self, e):

View File

@ -9,11 +9,11 @@ from PyQt4.QtGui import QAbstractSpinBox, QLineEdit, QCheckBox, QDialog, \
from calibre.gui2.dialogs.lrf_single_ui import Ui_LRFSingleDialog
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2 import qstring_to_unicode, error_dialog, \
pixmap_to_data, choose_images
pixmap_to_data, choose_images, config
from calibre.gui2.widgets import FontFamilyModel
from calibre.ebooks.lrf import option_parser
from calibre.ptempfile import PersistentTemporaryFile
from calibre import __appname__, Settings
from calibre.constants import __appname__
font_family_model = None
@ -109,7 +109,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
def load_saved_global_defaults(self):
cmdline = Settings().get('LRF conversion defaults')
cmdline = config['LRF_conversion_defaults']
if cmdline:
self.set_options_from_cmdline(cmdline)
@ -163,7 +163,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
def select_cover(self, checked):
files = choose_images(self, 'change cover dialog',
u'Choose cover for ' + qstring_to_unicode(self.gui_title.text()))
_('Choose cover for ') + qstring_to_unicode(self.gui_title.text()))
if not files:
return
_file = files[0]
@ -385,7 +385,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
cmdline.extend([u'--cover', self.cover_file.name])
self.cmdline = [unicode(i) for i in cmdline]
else:
Settings().set('LRF conversion defaults', cmdline)
config.set('LRF_conversion_defaults', cmdline)
QDialog.accept(self)
class LRFBulkDialog(LRFSingleDialog):

View File

@ -18,7 +18,8 @@ from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.password import PasswordDialog
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
from calibre import Settings, islinux
from calibre import islinux
from calibre.utils.config import prefs
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
@ -145,7 +146,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
self.remove_unused_series)
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
self.timeout = float(Settings().get('network timeout', 5))
self.timeout = float(prefs['network_timeout'])
self.title.setText(db.title(row))
isbn = db.isbn(self.id, index_is_id=True)
if not isbn:

View File

@ -1,12 +1,11 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from PyQt4.QtGui import QDialog, QLineEdit
from PyQt4.QtCore import QVariant, SIGNAL, Qt
from PyQt4.QtCore import SIGNAL, Qt
from calibre.gui2.dialogs.password_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode
from calibre import Settings
from calibre.gui2 import qstring_to_unicode, dynamic
class PasswordDialog(QDialog, Ui_Dialog):
@ -14,10 +13,12 @@ class PasswordDialog(QDialog, Ui_Dialog):
QDialog.__init__(self, window)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.cfg_key = re.sub(r'[^0-9a-zA-Z]', '_', name)
settings = Settings()
un = settings.get(name+': un', u'')
pw = settings.get(name+': pw', u'')
un = dynamic[self.cfg_key+'__un']
pw = dynamic[self.cfg_key+'__un']
if not un: un = ''
if not pw: pw = ''
self.gui_username.setText(un)
self.gui_password.setText(pw)
self.sname = name
@ -37,7 +38,6 @@ class PasswordDialog(QDialog, Ui_Dialog):
return qstring_to_unicode(self.gui_password.text())
def accept(self):
settings = Settings()
settings.set(self.sname+': un', unicode(self.gui_username.text()))
settings.set(self.sname+': pw', unicode(self.gui_password.text()))
dynamic.set(self.cfg_key+'__un', unicode(self.gui_username.text()))
dynamic.set(self.cfg_key+'__pw', unicode(self.gui_password.text()))
QDialog.accept(self)

View File

@ -1,415 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import traceback, logging, collections, time
from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL, Qt, \
QVariant, QThread
from PyQt4.QtGui import QIcon, QDialog
from calibre import detect_ncpus, Settings
from calibre.gui2 import NONE, error_dialog
from calibre.parallel import Server
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
class JobException(Exception):
pass
class Job(QThread):
''' Class to run a function in a separate thread with optional mutex based locking.'''
def __init__(self, id, description, slot, priority, func, *args, **kwargs):
'''
@param id: Number. Id of this thread.
@param description: String. Description of this job.
@param slot: The callable that should be called when the job is done.
@param priority: The priority with which this thread should be run
@param func: A callable that should be executed in this thread.
'''
QThread.__init__(self)
self.id = id
self.func = func
self.description = description if description else 'Job #' + str(self.id)
self.args = args
self.kwargs = kwargs
self.slot, self._priority = slot, priority
self.result = None
self.percent_done = 0
self.logger = logging.getLogger('Job #'+str(id))
self.logger.setLevel(logging.DEBUG)
self.is_locked = False
self.log = self.exception = self.last_traceback = None
self.connect_done_signal()
self.start_time = None
def start(self):
self.start_time = time.time()
QThread.start(self, self._priority)
def progress_update(self, val):
self.percent_done = val
self.emit(SIGNAL('status_update(int, int)'), self.id, int(val))
def formatted_log(self):
if self.log is None:
return ''
return '<h2>Log:</h2><pre>%s</pre>'%self.log
class DeviceJob(Job):
''' Jobs that involve communication with the device. '''
def run(self):
last_traceback, exception = None, None
try:
self.result = self.func(self.progress_update, *self.args, **self.kwargs)
except Exception, err:
exception = err
last_traceback = traceback.format_exc()
self.exception, self.last_traceback = exception, last_traceback
def formatted_error(self):
if self.exception is None:
return ''
ans = u'<p><b>%s</b>: %s</p>'%(self.exception.__class__.__name__, self.exception)
ans += '<h2>Traceback:</h2><pre>%s</pre>'%self.last_traceback
return ans
def notify(self):
self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.id, self.description, self.result, self.exception, self.last_traceback)
def connect_done_signal(self):
if self.slot is not None:
self.connect(self, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.slot, Qt.QueuedConnection)
class ConversionJob(Job):
''' Jobs that involve conversion of content.'''
def __init__(self, *args, **kwdargs):
Job.__init__(self, *args, **kwdargs)
self.log = ''
def run(self):
result = None
self.server.run_job(self.id, self.func, progress=self.progress,
args=self.args, kwdargs=self.kwargs,
output=self.output)
res = None
while res is None:
time.sleep(2)
res = self.server.result(self.id)
if res is None:
exception, tb = 'UnknownError: This should not have happened', ''
else:
result, exception, tb = res
self.result, self.last_traceback, self.exception = result, tb, exception
def output(self, msg):
if self.log is None:
self.log = ''
self.log += msg
self.emit(SIGNAL('output_received()'))
def formatted_log(self):
return '<h2>Log:</h2><pre>%s</pre>'%self.log
def notify(self):
self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.id, self.description, self.result, self.exception, self.last_traceback, self.log)
def connect_done_signal(self):
if self.slot is not None:
self.connect(self, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.slot, Qt.QueuedConnection)
def formatted_error(self):
if self.exception is None:
return ''
ans = u'<p><b>%s</b>:'%repr(self.exception)
ans += '<h2>Traceback:</h2><pre>%s</pre>'%self.last_traceback
return ans
def progress(self, percent, msg):
self.emit(SIGNAL('update_progress(int, PyQt_PyObject)'), self.id, percent)
class JobManager(QAbstractTableModel):
PRIORITY = {'Idle' : QThread.IdlePriority,
'Lowest': QThread.LowestPriority,
'Low' : QThread.LowPriority,
'Normal': QThread.NormalPriority
}
def __init__(self):
QAbstractTableModel.__init__(self)
self.waiting_jobs = collections.deque()
self.running_jobs = collections.deque()
self.finished_jobs = collections.deque()
self.add_queue = collections.deque()
self.update_lock = QMutex() # Protects write access to the above dequeues
self.next_id = 0
self.wait_icon = QVariant(QIcon(':/images/jobs.svg'))
self.running_icon = QVariant(QIcon(':/images/exec.svg'))
self.error_icon = QVariant(QIcon(':/images/dialog_error.svg'))
self.done_icon = QVariant(QIcon(':/images/ok.svg'))
self.process_server = Server()
self.ncpus = detect_ncpus()
self.timer_id = self.startTimer(500)
def terminate_device_jobs(self):
for job in self.running_jobs:
if isinstance(job, DeviceJob):
job.terminate()
def terminate_all_jobs(self):
for job in self.running_jobs:
try:
if isinstance(job, DeviceJob):
job.terminate()
except:
continue
self.process_server.killall()
def timerEvent(self, event):
if event.timerId() == self.timer_id:
self.update_lock.lock()
try:
refresh = False
while self.add_queue:
job = self.add_queue.pop()
self.waiting_jobs.append(job)
self.emit(SIGNAL('job_added(int)'), job.id, Qt.QueuedConnection)
refresh = True
for job in [job for job in self.running_jobs if job.isFinished()]:
self.running_jobs.remove(job)
self.finished_jobs.appendleft(job)
if job.result != self.process_server.KILL_RESULT:
job.notify()
job.running_time = time.time() - job.start_time
self.emit(SIGNAL('job_done(int)'), job.id)
refresh = True
cjs = list(self.running_conversion_jobs())
if len(cjs) < self.ncpus:
cj = None
for job in self.waiting_jobs:
if isinstance(job, ConversionJob):
cj = job
break
if cj is not None:
self.waiting_jobs.remove(cj)
cj.start()
self.running_jobs.append(cj)
refresh = True
djs = list(self.running_device_jobs())
if len(djs) == 0:
dj = None
for job in self.waiting_jobs:
if isinstance(job, DeviceJob):
dj = job
break
if dj is not None:
self.waiting_jobs.remove(dj)
dj.start()
self.running_jobs.append(dj)
refresh = True
if refresh:
self.reset()
if len(self.running_jobs) == 0:
self.emit(SIGNAL('no_more_jobs()'))
for i in range(len(self.running_jobs)):
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), self.index(i, 3), self.index(i, 3))
finally:
self.update_lock.unlock()
def has_jobs(self):
return len(self.waiting_jobs) + len(self.running_jobs) > 0
def has_device_jobs(self):
return len(tuple(self.running_device_jobs())) > 0
def running_device_jobs(self):
for job in self.running_jobs:
if isinstance(job, DeviceJob):
yield job
def running_conversion_jobs(self):
for job in self.running_jobs:
if isinstance(job, ConversionJob):
yield job
def update_progress(self, id, percent):
row = -1
for collection in (self.running_jobs, self.waiting_jobs, self.finished_jobs):
for job in collection:
row += 1
if job.id == id:
job.percent_done = percent
index = self.index(row, 2)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
return
def create_job(self, job_class, description, slot, priority, *args, **kwargs):
self.next_id += 1
id = self.next_id
job = job_class(id, description, slot, priority, *args, **kwargs)
job.server = self.process_server
QObject.connect(job, SIGNAL('status_update(int, int)'), self.status_update,
Qt.QueuedConnection)
self.connect(job, SIGNAL('update_progress(int, PyQt_PyObject)'),
self.update_progress, Qt.QueuedConnection)
self.update_lock.lock()
self.add_queue.append(job)
self.update_lock.unlock()
return job
def run_conversion_job(self, slot, callable, args=[], **kwargs):
'''
Run a conversion job.
@param slot: The function to call with the job result.
@param callable: The function to call to communicate with the device.
@param args: The arguments to pass to callable
@param kwargs: The keyword arguments to pass to callable
'''
desc = kwargs.pop('job_description', '')
if args and hasattr(args[0], 'append') and '--verbose' not in args[0]:
args[0].append('--verbose')
priority = self.PRIORITY[Settings().get('conversion job priority', 'Normal')]
job = self.create_job(ConversionJob, desc, slot, priority,
callable, *args, **kwargs)
return job.id
def run_device_job(self, slot, callable, *args, **kwargs):
'''
Run a job to communicate with the device.
@param slot: The function to call with the job result.
@param callable: The function to call to communicate with the device.
@param args: The arguments to pass to callable
@param kwargs: The keyword arguments to pass to callable
'''
desc = callable.__doc__ if callable.__doc__ else ''
desc += kwargs.pop('job_extra_description', '')
job = self.create_job(DeviceJob, desc, slot, QThread.NormalPriority,
callable, *args, **kwargs)
return job.id
def rowCount(self, parent):
return len(self.running_jobs) + len(self.waiting_jobs) + len(self.finished_jobs)
def columnCount(self, parent):
return 4
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
if section == 0: text = _("Job")
elif section == 1: text = _("Status")
elif section == 2: text = _("Progress")
elif section == 3: text = _('Running time')
return QVariant(text)
else:
return QVariant(section+1)
def row_to_job(self, row):
if row < len(self.running_jobs):
return self.running_jobs[row], 0
row -= len(self.running_jobs)
if row < len(self.waiting_jobs):
return self.waiting_jobs[row], 1
row -= len(self.running_jobs)
return self.finished_jobs[row], 2
def data(self, index, role):
if role not in (Qt.DisplayRole, Qt.DecorationRole):
return NONE
row, col = index.row(), index.column()
try:
job, status = self.row_to_job(row)
except IndexError:
return NONE
if role == Qt.DisplayRole:
if col == 0:
return QVariant(job.description)
if col == 1:
if status == 2:
st = _('Finished') if job.exception is None else _('Error')
else:
st = [_('Working'), _('Waiting')][status]
return QVariant(st)
if col == 2:
return QVariant(int(100*job.percent_done))
if col == 3:
if job.start_time is None:
return NONE
rtime = job.running_time if hasattr(job, 'running_time') else time.time() - job.start_time
return QVariant('%dm %ds'%(int(rtime)//60, int(rtime)%60))
if role == Qt.DecorationRole and col == 0:
if status == 1:
return self.wait_icon
if status == 0:
return self.running_icon
if status == 2:
if job.exception or job.result == self.process_server.KILL_RESULT:
return self.error_icon
return self.done_icon
return NONE
def status_update(self, id, progress):
for i in range(len(self.running_jobs)):
job = self.running_jobs[i]
if job.id == id:
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), self.index(i, 2), self.index(i, 3))
break
def kill_job(self, row, gui_parent):
job, status = self.row_to_job(row)
if isinstance(job, DeviceJob):
error_dialog(gui_parent, _('Cannot kill job'),
_('Cannot kill jobs that are communicating with the device as this may cause data corruption.')).exec_()
return
if status == 2:
error_dialog(gui_parent, _('Cannot kill job'),
_('Cannot kill already completed jobs.')).exec_()
return
if status == 1:
self.update_lock.lock()
try:
self.waiting_jobs.remove(job)
self.finished_jobs.append(job)
self.emit(SIGNAL('job_done(int)'), job.id)
job.result = self.process_server.KILL_RESULT
finally:
self.update_lock.unlock()
else:
self.process_server.kill(job.id)
self.reset()
if len(self.running_jobs) + len(self.waiting_jobs) == 0:
self.emit(SIGNAL('no_more_jobs()'))
class DetailView(QDialog, Ui_Dialog):
def __init__(self, parent, job):
QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowTitle(job.description)
self.job = job
self.connect(self.job, SIGNAL('output_received()'), self.update)
self.update()
def update(self):
txt = self.job.formatted_error() + self.job.formatted_log()
if not txt:
txt = 'No details available'
self.log.setHtml(txt)
vbar = self.log.verticalScrollBar()
vbar.setValue(vbar.maximum())

193
src/calibre/gui2/jobs2.py Normal file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Job management.
'''
import time
from PyQt4.QtCore import QAbstractTableModel, QVariant, QModelIndex, Qt, SIGNAL
from PyQt4.QtGui import QIcon, QDialog
from calibre.parallel import ParallelJob, Server
from calibre.gui2 import Dispatcher, error_dialog
from calibre.gui2.device import DeviceJob
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
NONE = QVariant()
class JobManager(QAbstractTableModel):
wait_icon = QVariant(QIcon(':/images/jobs.svg'))
running_icon = QVariant(QIcon(':/images/exec.svg'))
error_icon = QVariant(QIcon(':/images/dialog_error.svg'))
done_icon = QVariant(QIcon(':/images/ok.svg'))
def __init__(self):
QAbstractTableModel.__init__(self)
self.jobs = []
self.server = Server()
self.add_job = Dispatcher(self._add_job)
self.status_update = Dispatcher(self._status_update)
self.start_work = Dispatcher(self._start_work)
self.job_done = Dispatcher(self._job_done)
def columnCount(self, parent=QModelIndex()):
return 4
def rowCount(self, parent=QModelIndex()):
return len(self.jobs)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
if section == 0: text = _("Job")
elif section == 1: text = _("Status")
elif section == 2: text = _("Progress")
elif section == 3: text = _('Running time')
return QVariant(text)
else:
return QVariant(section+1)
def data(self, index, role):
try:
if role not in (Qt.DisplayRole, Qt.DecorationRole):
return NONE
row, col = index.row(), index.column()
job = self.jobs[row]
if role == Qt.DisplayRole:
if col == 0:
desc = job.description
if not desc:
desc = _('Unknown job')
return QVariant(desc)
if col == 1:
status = job.status()
if status == 'DONE':
st = _('Finished')
elif status == 'ERROR':
st = _('Error')
elif status == 'WAITING':
st = _('Waiting')
else:
st = _('Working')
return QVariant(st)
if col == 2:
pc = job.percent
if pc <=0:
percent = 0
else:
percent = int(100*pc)
return QVariant(percent)
if col == 3:
if job.start_time is None:
return NONE
rtime = job.running_time if job.running_time is not None else \
time.time() - job.start_time
return QVariant('%dm %ds'%(int(rtime)//60, int(rtime)%60))
if role == Qt.DecorationRole and col == 0:
status = job.status()
if status == 'WAITING':
return self.wait_icon
if status == 'WORKING':
return self.running_icon
if status == 'ERROR':
return self.error_icon
if status == 'DONE':
return self.done_icon
except:
import traceback
traceback.print_exc()
return NONE
def _add_job(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.append(job)
self.jobs.sort()
self.emit(SIGNAL('job_added(int)'), self.rowCount())
self.emit(SIGNAL('layoutChanged()'))
def done_jobs(self):
return [j for j in self.jobs if j.status() in ['DONE', 'ERROR']]
def row_to_job(self, row):
return self.jobs[row]
def _start_work(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.sort()
self.emit(SIGNAL('layoutChanged()'))
def _job_done(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.sort()
self.emit(SIGNAL('job_done(int)'), len(self.jobs) - len(self.done_jobs()))
self.emit(SIGNAL('layoutChanged()'))
def _status_update(self, job):
row = self.jobs.index(job)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 0), self.index(row, 3))
def has_device_jobs(self):
for job in self.jobs:
if job.is_running and isinstance(job, DeviceJob):
return True
return False
def has_jobs(self):
for job in self.jobs:
if job.is_running:
return True
return False
def run_job(self, done, func, args=[], kwargs={},
description=None):
job = ParallelJob(func, done, self, args=args, kwargs=kwargs,
description=description)
self.server.add_job(job)
return job
def output(self, job):
self.emit(SIGNAL('output_received()'))
def kill_job(self, row, view):
job = self.jobs[row]
if isinstance(job, DeviceJob):
error_dialog(view, _('Cannot kill job'),
_('Cannot kill jobs that communicate with the device')).exec_()
return
if job.has_run:
error_dialog(view, _('Cannot kill job'),
_('Job has already run')).exec_()
return
if not job.is_running:
error_dialog(view, _('Cannot kill job'),
_('Cannot kill waiting job')).exec_()
return
self.server.kill(job)
def terminate_all_jobs(self):
pass
class DetailView(QDialog, Ui_Dialog):
def __init__(self, parent, job):
QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowTitle(job.description)
self.job = job
self.update()
def update(self):
self.log.setHtml(self.job.gui_text())
vbar = self.log.verticalScrollBar()
vbar.setValue(vbar.maximum())

View File

@ -13,10 +13,10 @@ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor,
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
from calibre import Settings, preferred_encoding
from calibre import preferred_encoding
from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.database import LibraryDatabase, text_to_tokens
from calibre.gui2 import NONE, TableView, qstring_to_unicode
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config
class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue")
@ -113,7 +113,7 @@ class BooksModel(QAbstractTableModel):
self.cover_cache.clear_cache()
def read_config(self):
self.use_roman_numbers = Settings().get('use roman numerals for series number', True)
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
def set_database(self, db):

View File

@ -6,10 +6,11 @@ import sys, logging, os, traceback, time
from PyQt4.QtGui import QKeySequence, QPainter, QDialog, QSpinBox, QSlider, QIcon
from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread
from calibre import __appname__, setup_cli_handlers, islinux, Settings
from calibre import __appname__, setup_cli_handlers, islinux
from calibre.ebooks.lrf.lrfparser import LRFDocument
from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, choose_files, Application
from calibre.gui2 import ORG_NAME, APP_UID, error_dialog, \
config, choose_files, Application
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
@ -102,13 +103,15 @@ class Main(MainWindow, Ui_MainWindow):
def configure(self, triggered):
opts = Settings().get('LRF ebook viewer options', self.opts)
opts = config['LRF_ebook_viewer_options']
if not opts:
opts = self.opts
d = Config(self, opts)
d.exec_()
if d.result() == QDialog.Accepted:
opts.white_background = bool(d.white_background.isChecked())
opts.hyphenate = bool(d.hyphenate.isChecked())
Settings().set('LRF ebook viewer options', opts)
config['LRF_ebook_viewer_options'] = opts
def set_ebook(self, stream):
self.progress_bar.setMinimum(0)
@ -281,7 +284,9 @@ Read the LRF ebook book.lrf
return parser
def normalize_settings(parser, opts):
saved_opts = Settings().get('LRF ebook viewer options', opts)
saved_opts = config['LRF_ebook_viewer_options']
if not saved_opts:
saved_opts = opts
for opt in parser.option_list:
if not opt.dest:
continue

View File

@ -3,31 +3,32 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, textwrap, collections, traceback, time
from xml.parsers.expat import ExpatError
from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QVariant, QThread, QUrl, QSize
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog, QDesktopServices
from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
Settings, iswindows, isosx, preferred_encoding
iswindows, isosx, preferred_encoding
from calibre.ptempfile import PersistentTemporaryFile
from calibre.ebooks.metadata.meta import get_metadata, get_filename_pat, set_filename_pat
from calibre.ebooks.metadata.meta import get_metadata
from calibre.devices.errors import FreeSpaceError
from calibre.devices.interface import Device
from calibre.utils.config import prefs, dynamic
from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
initialize_file_icon_provider, question_dialog,\
pixmap_to_data, choose_dir, ORG_NAME, \
set_sidebar_directories, \
SingleApplication, Application, available_height, max_available_height
set_sidebar_directories, Dispatcher, \
SingleApplication, Application, available_height, \
max_available_height, config
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
from calibre.library.database import LibraryDatabase
from calibre.gui2.update import CheckForUpdates
from calibre.gui2.main_window import MainWindow, option_parser
from calibre.gui2.main_ui import Ui_MainWindow
from calibre.gui2.device import DeviceDetector, DeviceManager
from calibre.gui2.device import DeviceManager
from calibre.gui2.status import StatusBar
from calibre.gui2.jobs import JobManager
from calibre.gui2.jobs2 import JobManager
from calibre.gui2.news import NewsMenu
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
@ -45,6 +46,8 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.parallel import JobKilled
from calibre.utils.filenames import ascii_filename
class Main(MainWindow, Ui_MainWindow):
@ -73,7 +76,6 @@ class Main(MainWindow, Ui_MainWindow):
self.read_settings()
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
self.device_manager = None
self.upload_memory = {}
self.delete_memory = {}
self.conversion_jobs = {}
@ -120,12 +122,13 @@ class Main(MainWindow, Ui_MainWindow):
sm.addAction(_('Send to storage card by default'))
sm.actions()[-1].setCheckable(True)
def default_sync(checked):
Settings().set('send to device by default', bool(checked))
config.set('send_to_device_by_default', bool(checked))
QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_main_memory)
QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card)
QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card if checked else self.sync_to_main_memory)
QObject.connect(sm.actions()[-1], SIGNAL('toggled(bool)'), default_sync)
sm.actions()[-1].setChecked(Settings().get('send to device by default', False))
sm.actions()[-1].setChecked(config.get('send_to_device_by_default'))
default_sync(sm.actions()[-1].isChecked())
self.sync_menu = sm # Needed
md = QMenu()
@ -152,7 +155,7 @@ class Main(MainWindow, Ui_MainWindow):
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'))
self.save_menu.addAction(_('Save to disk in a single directory'))
self.save_menu.addAction(_('Save only %s format to disk')%Settings().get('save to disk single format', 'lrf').upper())
self.save_menu.addAction(_('Save only %s format to disk')%config.get('save_to_disk_single_format').upper())
self.view_menu = QMenu()
self.view_menu.addAction(_('View'))
@ -252,10 +255,8 @@ class Main(MainWindow, Ui_MainWindow):
self.setMaximumHeight(max_available_height())
####################### Setup device detection ########################
self.detector = DeviceDetector(sleep_time=2000)
QObject.connect(self.detector, SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'),
self.device_detected, Qt.QueuedConnection)
self.detector.start(QThread.InheritPriority)
self.device_manager = DeviceManager(Dispatcher(self.device_detected), self.job_manager)
self.device_manager.start()
self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds())
@ -314,22 +315,19 @@ class Main(MainWindow, Ui_MainWindow):
########################## Connect to device ##############################
def device_detected(self, device, connected):
def device_detected(self, connected):
'''
Called when a device is connected to the computer.
'''
if connected:
self.device_manager = DeviceManager(device)
self.job_manager.run_device_job(self.info_read, self.device_manager.info_func())
self.set_default_thumbnail(device.THUMBNAIL_HEIGHT)
self.status_bar.showMessage(_('Device: ')+device.__class__.__name__+_(' detected.'), 3000)
self.device_manager.get_device_information(Dispatcher(self.info_read))
self.set_default_thumbnail(self.device_manager.device.THUMBNAIL_HEIGHT)
self.status_bar.showMessage(_('Device: ')+\
self.device_manager.device.__class__.__name__+_(' detected.'), 3000)
self.action_sync.setEnabled(True)
self.device_connected = True
else:
self.device_connected = False
self.job_manager.terminate_device_jobs()
if self.device_manager:
self.device_manager.device_removed()
self.location_view.model().update_devices()
self.action_sync.setEnabled(False)
self.vanity.setText(self.vanity_template%dict(version=self.latest_version, device=' '))
@ -338,27 +336,26 @@ class Main(MainWindow, Ui_MainWindow):
self.status_bar.reset_info()
self.location_selected('library')
def info_read(self, id, description, result, exception, formatted_traceback):
def info_read(self, job):
'''
Called once device information has been read.
'''
if exception:
self.device_job_exception(id, description, exception, formatted_traceback)
if job.exception is not None:
self.device_job_exception(job)
return
info, cp, fs = result
info, cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.device_info = _('Connected ')+' '.join(info[:-1])
self.vanity.setText(self.vanity_template%dict(version=self.latest_version, device=self.device_info))
func = self.device_manager.books_func()
self.job_manager.run_device_job(self.metadata_downloaded, func)
def metadata_downloaded(self, id, description, result, exception, formatted_traceback):
self.device_manager.books(Dispatcher(self.metadata_downloaded))
def metadata_downloaded(self, job):
'''
Called once metadata has been read for all books on the device.
'''
if exception:
print exception, type(exception)
if isinstance(exception, ExpatError):
if job.exception is not None:
if isinstance(job.exception, ExpatError):
error_dialog(self, _('Device database corrupted'),
_('''
<p>The database of books on the reader is corrupted. Try the following:
@ -368,9 +365,9 @@ class Main(MainWindow, Ui_MainWindow):
</ol>
''')%dict(app=__appname__)).exec_()
else:
self.device_job_exception(id, description, exception, formatted_traceback)
self.device_job_exception(job)
return
mainlist, cardlist = result
mainlist, cardlist = job.result
self.memory_view.set_database(mainlist)
self.card_view.set_database(cardlist)
for view in (self.memory_view, self.card_view):
@ -387,18 +384,17 @@ class Main(MainWindow, Ui_MainWindow):
'''
Upload metadata to device.
'''
self.job_manager.run_device_job(self.metadata_synced,
self.device_manager.sync_booklists_func(),
self.booklists())
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
self.booklists())
def metadata_synced(self, id, description, result, exception, formatted_traceback):
def metadata_synced(self, job):
'''
Called once metadata has been uploaded.
'''
if exception:
self.device_job_exception(id, description, exception, formatted_traceback)
if job.exception is not None:
self.device_job_exception(job)
return
cp, fs = result
cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
############################################################################
@ -500,35 +496,34 @@ class Main(MainWindow, Ui_MainWindow):
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
Upload books to device.
@param files: List of either paths to files or file like objects
:param files: List of either paths to files or file like objects
'''
titles = ', '.join([i['title'] for i in metadata])
id = self.job_manager.run_device_job(self.books_uploaded,
self.device_manager.upload_books_func(),
titles = [i['title'] for i in metadata]
job = self.device_manager.upload_books(Dispatcher(self.books_uploaded),
files, names, on_card=on_card,
job_extra_description=titles
titles=titles
)
self.upload_memory[id] = (metadata, on_card, memory)
self.upload_memory[job] = (metadata, on_card, memory)
def books_uploaded(self, id, description, result, exception, formatted_traceback):
def books_uploaded(self, job):
'''
Called once books have been uploaded.
'''
metadata, on_card, memory = self.upload_memory.pop(id)
metadata, on_card, memory = self.upload_memory.pop(job)
if exception:
if isinstance(exception, FreeSpaceError):
where = 'in main memory.' if 'memory' in str(exception) else 'on the storage card.'
if job.exception is not None:
if isinstance(job.exception, FreeSpaceError):
where = 'in main memory.' if 'memory' in str(job.exception) else 'on the storage card.'
titles = '\n'.join(['<li>'+mi['title']+'</li>' for mi in metadata])
d = error_dialog(self, _('No space on device'),
_('<p>Cannot upload books to device there is no more free space available ')+where+
'</p>\n<ul>%s</ul>'%(titles,))
d.exec_()
else:
self.device_job_exception(id, description, exception, formatted_traceback)
self.device_job_exception(job)
return
self.device_manager.add_books_to_metadata(result, metadata, self.booklists())
self.device_manager.add_books_to_metadata(job.result, metadata, self.booklists())
self.upload_booklists()
@ -551,7 +546,7 @@ class Main(MainWindow, Ui_MainWindow):
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return
if Settings().get('confirm delete', False):
if config['confirm_delete']:
d = question_dialog(self, _('Confirm delete'),
_('Are you sure you want to delete these %d books?')%len(rows))
if d.exec_() != QMessageBox.Yes:
@ -568,22 +563,20 @@ class Main(MainWindow, Ui_MainWindow):
self.status_bar.showMessage(_('Deleting books from device.'), 1000)
def remove_paths(self, paths):
return self.job_manager.run_device_job(self.books_deleted,
self.device_manager.delete_books_func(), paths)
return self.device_manager.delete_books(Dispatcher(self.books_deleted), paths)
def books_deleted(self, id, description, result, exception, formatted_traceback):
def books_deleted(self, job):
'''
Called once deletion is done on the device
'''
for view in (self.memory_view, self.card_view):
view.model().deletion_done(id, bool(exception))
if exception:
self.device_job_exception(id, description, exception, formatted_traceback)
view.model().deletion_done(id, bool(job.exception))
if job.exception is not None:
self.device_job_exception(job)
return
if self.delete_memory.has_key(id):
paths, model = self.delete_memory.pop(id)
if self.delete_memory.has_key(job):
paths, model = self.delete_memory.pop(job)
self.device_manager.remove_books_from_metadata(paths, self.booklists())
model.paths_deleted(paths)
self.upload_booklists()
@ -686,10 +679,9 @@ class Main(MainWindow, Ui_MainWindow):
if not a:
a = 'Unknown'
prefix = sanitize_file_name(t+' - '+a)
if isinstance(prefix, unicode):
prefix = prefix.encode('ascii', 'ignore')
else:
prefix = prefix.decode('ascii', 'ignore').encode('ascii', 'ignore')
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
remove = [self.library_view.model().id(r) for r in rows] if delete_from_library else []
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
@ -705,7 +697,7 @@ class Main(MainWindow, Ui_MainWindow):
############################## Save to disk ################################
def save_single_format_to_disk(self, checked):
self.save_to_disk(checked, True, Settings().get('save to disk single format', 'lrf'))
self.save_to_disk(checked, True, config['save_to_disk_single_format'])
def save_to_single_dir(self, checked):
self.save_to_disk(checked, True)
@ -732,12 +724,11 @@ class Main(MainWindow, Ui_MainWindow):
QDesktopServices.openUrl(QUrl('file:'+dir))
else:
paths = self.current_view().model().paths(rows)
self.job_manager.run_device_job(self.books_saved,
self.device_manager.save_books_func(), paths, dir)
self.device_manager.save_books(Dispatcher(self.books_saved), paths, dir)
def books_saved(self, id, description, result, exception, formatted_traceback):
if exception:
self.device_job_exception(id, description, exception, formatted_traceback)
def books_saved(self, job):
if job.exception is not None:
self.device_job_exception(job)
return
############################################################################
@ -760,15 +751,15 @@ class Main(MainWindow, Ui_MainWindow):
if data['password']:
args.extend(['--password', data['password']])
args.append(data['script'] if data['script'] else data['title'])
id = self.job_manager.run_conversion_job(self.news_fetched, 'feeds2lrf', args=[args],
job_description=_('Fetch news from ')+data['title'])
self.conversion_jobs[id] = (pt, 'lrf')
job = self.job_manager.run_job(Dispatcher(self.news_fetched), 'feeds2lrf', args=[args],
description=_('Fetch news from ')+data['title'])
self.conversion_jobs[job] = (pt, 'lrf')
self.status_bar.showMessage(_('Fetching news from ')+data['title'], 2000)
def news_fetched(self, id, description, result, exception, formatted_traceback, log):
pt, fmt = self.conversion_jobs.pop(id)
if exception:
self.conversion_job_exception(id, description, exception, formatted_traceback, log)
def news_fetched(self, job):
pt, fmt = self.conversion_jobs.pop(job)
if job.exception is not None:
self.job_exception(job)
return
to_device = self.device_connected and fmt in self.device_manager.device_class.FORMATS
self._add_books([pt.name], to_device)
@ -806,8 +797,9 @@ class Main(MainWindow, Ui_MainWindow):
bad_rows = []
self.status_bar.showMessage(_('Starting Bulk conversion of %d books')%len(rows), 2000)
for i, row in enumerate([r.row() for r in rows]):
if rows and hasattr(rows[0], 'row'):
rows = [r.row() for r in rows]
for i, row in enumerate(rows):
cmdline = list(d.cmdline)
mi = self.library_view.model().db.get_metadata(row)
if mi.title:
@ -841,12 +833,12 @@ class Main(MainWindow, Ui_MainWindow):
cmdline.extend(['--cover', cf.name])
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted,
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'any2lrf', args=[cmdline],
job_description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title)))
description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title)))
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format,
self.conversion_jobs[job] = (d.cover_file, pt, of, d.output_format,
self.library_view.model().db.id(row))
res = []
for row in bad_rows:
@ -887,10 +879,10 @@ class Main(MainWindow, Ui_MainWindow):
setattr(options, 'output', of.name)
options.verbose = 1
args = [pt.name, options]
id = self.job_manager.run_conversion_job(self.book_converted,
'comic2lrf', args=args,
job_description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title)))
self.conversion_jobs[id] = (None, pt, of, 'lrf',
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'comic2lrf', args=args,
description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title)))
self.conversion_jobs[job] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
@ -917,12 +909,12 @@ class Main(MainWindow, Ui_MainWindow):
of.close()
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted,
'any2lrf', args=[cmdline],
job_description=_('Convert book: ')+d.title())
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'any2lrf', args=[cmdline],
description=_('Convert book: ')+d.title())
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, d.id)
self.conversion_jobs[job] = (d.cover_file, pt, of, d.output_format, d.id)
changed = True
if changed:
self.library_view.model().resort(reset=False)
@ -962,24 +954,24 @@ class Main(MainWindow, Ui_MainWindow):
opts.verbose = 1
args = [pt.name, opts]
changed = True
id = self.job_manager.run_conversion_job(self.book_converted,
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'comic2lrf', args=args,
job_description=_('Convert comic: ')+opts.title)
self.conversion_jobs[id] = (None, pt, of, 'lrf',
description=_('Convert comic: ')+opts.title)
self.conversion_jobs[job] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
if changed:
self.library_view.model().resort(reset=False)
self.library_view.model().research()
def book_converted(self, id, description, result, exception, formatted_traceback, log):
of, fmt, book_id = self.conversion_jobs.pop(id)[2:]
if exception:
self.conversion_job_exception(id, description, exception, formatted_traceback, log)
def book_converted(self, job):
of, fmt, book_id = self.conversion_jobs.pop(job)[2:]
if job.exception is not None:
self.job_exception(job)
return
data = open(of.name, 'rb')
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
data.close()
self.status_bar.showMessage(description + (' completed'), 2000)
self.status_bar.showMessage(job.description + (' completed'), 2000)
#############################View book######################################
@ -990,19 +982,18 @@ class Main(MainWindow, Ui_MainWindow):
self.persistent_files.append(pt)
self._view_file(pt.name)
def book_downloaded_for_viewing(self, id, description, result, exception, formatted_traceback):
if exception:
self.device_job_exception(id, description, exception, formatted_traceback)
def book_downloaded_for_viewing(self, job):
if job.exception:
self.device_job_exception(job)
return
print result
self._view_file(result)
self._view_file(job.result)
def _view_file(self, name):
self.setCursor(Qt.BusyCursor)
try:
if name.upper().endswith('.LRF'):
args = ['lrfviewer', name]
self.job_manager.process_server.run_free_job('lrfviewer', kwdargs=dict(args=args))
self.job_manager.server.run_free_job('lrfviewer', kwdargs=dict(args=args))
else:
QDesktopServices.openUrl(QUrl('file:'+name))#launch(name)
time.sleep(5) # User feedback
@ -1063,8 +1054,8 @@ class Main(MainWindow, Ui_MainWindow):
pt = PersistentTemporaryFile('_viewer_'+os.path.splitext(paths[0])[1])
self.persistent_files.append(pt)
pt.close()
self.job_manager.run_device_job(self.book_downloaded_for_viewing,
self.device_manager.view_book_func(), paths[0], pt.name)
self.device_manager.view_book(Dispatcher(self.book_downloaded_for_viewing),
paths[0], pt.name)
@ -1093,9 +1084,8 @@ class Main(MainWindow, Ui_MainWindow):
d.exec_()
if d.result() == d.Accepted:
self.library_view.set_visible_columns(d.final_columns)
settings = Settings()
self.tool_bar.setIconSize(settings.value('toolbar icon size', QVariant(QSize(48, 48))).toSize())
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly)
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly)
if self.library_path != d.database_location:
try:
@ -1116,7 +1106,7 @@ class Main(MainWindow, Ui_MainWindow):
_('<p>An invalid database already exists at %s, delete it before trying to move the existing database.<br>Error: %s')%(newloc, str(err)))
d.exec_()
self.library_path = self.library_view.model().db.library_path
Settings().set('library path', self.library_path)
prefs['library path'] = self.library_path
except Exception, err:
traceback.print_exc()
d = error_dialog(self, _('Could not move database'), unicode(err))
@ -1177,79 +1167,47 @@ class Main(MainWindow, Ui_MainWindow):
self.action_convert.setEnabled(False)
self.view_menu.actions()[1].setEnabled(False)
def device_job_exception(self, id, description, exception, formatted_traceback):
def device_job_exception(self, job):
'''
Handle exceptions in threaded device jobs.
'''
if 'Could not read 32 bytes on the control bus.' in str(exception):
if 'Could not read 32 bytes on the control bus.' in str(job.exception):
error_dialog(self, _('Error talking to device'),
_('There was a temporary error talking to the device. Please unplug and reconnect the device and or reboot.')).show()
return
print >>sys.stderr, 'Error in job:', description.encode('utf8')
print >>sys.stderr, exception
print >>sys.stderr, formatted_traceback.encode('utf8')
try:
print >>sys.stderr, job.console_text()
except:
pass
if not self.device_error_dialog.isVisible():
msg = u'<p><b>%s</b>: '%(exception.__class__.__name__,) + unicode(str(exception), 'utf8', 'replace') + u'</p>'
msg += u'<p>Failed to perform <b>job</b>: '+description
msg += u'<p>Further device related error messages will not be shown while this message is visible.'
msg += u'<p>Detailed <b>traceback</b>:<pre>'
if isinstance(formatted_traceback, str):
formatted_traceback = unicode(formatted_traceback, 'utf8', 'replace')
msg += formatted_traceback
self.device_error_dialog.set_message(msg)
self.device_error_dialog.set_message(job.gui_text())
self.device_error_dialog.show()
def conversion_job_exception(self, id, description, exception, formatted_traceback, log):
def job_exception(self, job):
def safe_print(msgs, file=sys.stderr):
for i, msg in enumerate(msgs):
if not msg:
msg = ''
if isinstance(msg, unicode):
msgs[i] = msg.encode(preferred_encoding, 'replace')
msg = ' '.join(msgs)
print >>file, msg
def safe_unicode(arg):
if not arg:
arg = unicode(repr(arg))
if isinstance(arg, str):
arg = arg.decode(preferred_encoding, 'replace')
if not isinstance(arg, unicode):
try:
arg = unicode(repr(arg))
except:
arg = u'Could not convert to unicode'
return arg
only_msg = getattr(exception, 'only_msg', False)
description, exception, formatted_traceback, log = map(safe_unicode,
(description, exception, formatted_traceback, log))
only_msg = getattr(job.exception, 'only_msg', False)
try:
safe_print('Error in job:', description)
if log:
safe_print(log)
safe_print(exception)
safe_print(formatted_traceback)
print job.console_text()
except:
pass
if only_msg:
error_dialog(self, _('Conversion Error'), exception).exec_()
try:
exc = unicode(job.exception)
except:
exc = repr(job.exception)
error_dialog(self, _('Conversion Error'), exc).exec_()
return
msg = u'<p><b>%s</b>:'%exception
msg += u'<p>Failed to perform <b>job</b>: '+description
msg += u'<p>Detailed <b>traceback</b>:<pre>'
msg += formatted_traceback + u'</pre>'
msg += u'<p><b>Log:</b></p><pre>'
msg += log
ConversionErrorDialog(self, 'Conversion Error', msg, show=True)
if isinstance(job.exception, JobKilled):
return
ConversionErrorDialog(self, _('Conversion Error'), job.gui_text(),
show=True)
def initialize_database(self, settings):
self.library_path = settings.get('library path', None)
def initialize_database(self):
self.library_path = prefs['library path']
self.olddb = None
if self.library_path is None: # Need to migrate to new database layout
self.database_path = settings.get('database path')
self.database_path = prefs['database_path']
if not os.access(os.path.dirname(self.database_path), os.W_OK):
error_dialog(self, _('Database does not exist'), _('The directory in which the database should be: %s no longer exists. Please choose a new database location.')%self.database_path).exec_()
self.database_path = choose_dir(self, 'database path dialog', 'Choose new location for database')
@ -1272,28 +1230,20 @@ class Main(MainWindow, Ui_MainWindow):
def read_settings(self):
settings = Settings()
settings.beginGroup('Main Window')
geometry = settings.value('main window geometry', QVariant()).toByteArray()
self.restoreGeometry(geometry)
settings.endGroup()
self.initialize_database(settings)
self.initialize_database()
geometry = config['main_window_geometry']
if geometry is not None:
self.restoreGeometry(geometry)
set_sidebar_directories(None)
set_filename_pat(settings.get('filename pattern', get_filename_pat()))
self.tool_bar.setIconSize(settings.get('toolbar icon size', QSize(48, 48)))
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if settings.get('show text in toolbar', True) else Qt.ToolButtonIconOnly)
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly)
def write_settings(self):
settings = Settings()
settings.beginGroup("Main Window")
settings.setValue("main window geometry", QVariant(self.saveGeometry()))
settings.endGroup()
settings.beginGroup('Book Views')
config.set('main_window_geometry', self.saveGeometry())
self.library_view.write_settings()
if self.device_connected:
self.memory_view.write_settings()
settings.endGroup()
def closeEvent(self, e):
msg = 'There are active jobs. Are you sure you want to quit?'
@ -1312,11 +1262,10 @@ class Main(MainWindow, Ui_MainWindow):
self.job_manager.terminate_all_jobs()
self.write_settings()
self.detector.keep_going = False
self.device_manager.keep_going = False
self.cover_cache.stop()
self.hide()
time.sleep(2)
self.detector.terminate()
self.cover_cache.terminate()
e.accept()
@ -1327,17 +1276,17 @@ class Main(MainWindow, Ui_MainWindow):
self.vanity.setText(self.vanity_template%(dict(version=self.latest_version,
device=self.device_info)))
self.vanity.update()
s = Settings()
if s.get('new version notification', True) and s.get('update to version %s'%version, True):
if config.get('new_version_notification') and dynamic.get('update to version %s'%version, True):
d = question_dialog(self, _('Update available'), _('%s has been updated to version %s. See the <a href="http://calibre.kovidgoyal.net/wiki/Changelog">new features</a>. Visit the download page?')%(__appname__, version))
if d.exec_() == QMessageBox.Yes:
url = 'http://calibre.kovidgoyal.net/download_'+('windows' if iswindows else 'osx' if isosx else 'linux')
QDesktopServices.openUrl(QUrl(url))
s.set('update to version %s'%version, False)
dynamic.set('update to version %s'%version, False)
def main(args=sys.argv):
from calibre import singleinstance
from calibre.utils.lock import singleinstance
pid = os.fork() if False and islinux else -1
if pid <= 0:
parser = option_parser('''\

View File

@ -3,9 +3,9 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import StringIO, traceback, sys
from PyQt4.Qt import QMainWindow, QString, Qt, QFont
from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre import OptionParser
from calibre.utils.config import OptionParser
def option_parser(usage='''\
Usage: %prog [options]
@ -36,11 +36,17 @@ class MainWindow(QMainWindow):
def __init__(self, opts, parent=None):
QMainWindow.__init__(self, parent)
app = QCoreApplication.instance()
if app is not None:
self.connect(app, SIGNAL('unixSignal(int)'), self.unix_signal)
if getattr(opts, 'redirect', False):
self.__console_redirect = DebugWindow(self)
sys.stdout = sys.stderr = self.__console_redirect
self.__console_redirect.show()
def unix_signal(self, signal):
print 'Received signal:', repr(signal)
def unhandled_exception(self, type, value, tb):
try:
sio = StringIO.StringIO()

View File

@ -39,9 +39,14 @@ def build_forms(forms):
dat = dat.replace('import images_rc', 'from calibre.gui2 import images_rc')
dat = dat.replace('from library import', 'from calibre.gui2.library import')
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import')
#dat += '\nfrom calibre.gui2 import TranslatedDialogButtonBox'
dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
#dat = re.compile(r'QtGui.QDialogButtonBox').sub('TranslatedDialogButtonBox', dat)
# Workaround bug in Qt 4.4 on Windows
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
print 'Implementing Workaround for buggy pyuic in form', form
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
open(compiled_form, 'wb').write(dat)

View File

@ -1,52 +0,0 @@
import os, sys, glob
if os.environ.get('PYQT4PATH', None):
print os.environ['PYQT4PATH']
sys.path.insert(0, os.environ['PYQT4PATH'])
from PyQt4 import pyqtconfig
# The name of the SIP build file generated by SIP and used by the build
# system.
build_file = "pictureflow.sbf"
# Get the PyQt configuration information.
config = pyqtconfig.Configuration()
# Run SIP to generate the code. Note that we tell SIP where to find the qt
# module's specification files using the -I flag.
sip = [config.sip_bin, "-c", ".", "-b", build_file, "-I",
config.pyqt_sip_dir, config.pyqt_sip_flags, "../pictureflow.sip"]
os.system(" ".join(sip))
installs=[]
# Create the Makefile. The QtModuleMakefile class provided by the
# pyqtconfig module takes care of all the extra preprocessor, compiler and
# linker flags needed by the Qt library.
makefile = pyqtconfig.QtGuiModuleMakefile (
configuration=config,
build_file=build_file,
installs=installs,
qt=1,
)
# Setup the platform dependent Makefile parameters
d = os.path.dirname
if 'darwin' in sys.platform:
makefile.extra_cflags += ['-arch i386', '-arch ppc']
makefile.extra_lflags += ['-arch i386', '-arch ppc']
qtdir = os.path.join(d(d(os.getcwd())), '.build')
if 'win32' in sys.platform:
qtdir = os.path.join(qtdir, 'release')
makefile.extra_lib_dirs += ['C:/Python25/libs']
# Add the compiled Qt objects
qtobjs = map(lambda x:'"'+x+'"', glob.glob(os.path.join(qtdir, '*.o')))
makefile.extra_lflags += qtobjs
makefile.extra_cxxflags = makefile.extra_cflags
# Generate the Makefile itself.
makefile.generate()

View File

@ -1,6 +0,0 @@
TARGET = pictureflow
TEMPLATE = lib
HEADERS = pictureflow.h
SOURCES = pictureflow.cpp
VERSION = 1.0.0
CONFIG += x86 ppc

View File

@ -9,7 +9,7 @@
class FlowImages : QObject {
%TypeHeaderCode
#include "../../pictureflow.h"
#include <pictureflow.h>
%End
public:
@ -22,7 +22,7 @@ public:
class PictureFlow : QWidget {
%TypeHeaderCode
#include "../../pictureflow.h"
#include <pictureflow.h>
%End

View File

@ -163,24 +163,23 @@ class StatusBar(QStatusBar):
def show_book_info(self):
self.emit(SIGNAL('show_book_info()'))
def job_added(self, id):
def job_added(self, nnum):
jobs = self.movie_button.jobs
src = qstring_to_unicode(jobs.text())
num = self.jobs()
nnum = num+1
nnum = num + 1
text = src.replace(str(num), str(nnum))
jobs.setText(text)
if self.movie_button.movie.state() == QMovie.Paused:
self.movie_button.movie.setPaused(False)
def job_done(self, id):
def job_done(self, running):
jobs = self.movie_button.jobs
src = qstring_to_unicode(jobs.text())
num = self.jobs()
nnum = num-1
text = src.replace(str(num), str(nnum))
text = src.replace(str(num), str(running))
jobs.setText(text)
if nnum == 0:
if running == 0:
self.no_more_jobs()
def no_more_jobs(self):

View File

@ -9,15 +9,16 @@ from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
QSyntaxHighlighter, QCursor, QColor, QWidget, \
QAbstractItemDelegate, QPixmap, QStyle, QFontMetrics
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
QObject, QRegExp, QString
QObject, QRegExp, QString, QSettings
from calibre.gui2.jobs import DetailView
from calibre.gui2 import human_readable, NONE, TableView, qstring_to_unicode, error_dialog
from calibre.gui2.jobs2 import DetailView
from calibre.gui2 import human_readable, NONE, TableView, \
qstring_to_unicode, error_dialog
from calibre.gui2.filename_pattern_ui import Ui_Form
from calibre import fit_image, Settings
from calibre import fit_image
from calibre.utils.fontconfig import find_font_families
from calibre.ebooks.metadata.meta import get_filename_pat, metadata_from_filename, \
set_filename_pat
from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs
@ -29,7 +30,7 @@ class FilenamePattern(QWidget, Ui_Form):
self.connect(self.test_button, SIGNAL('clicked()'), self.do_test)
self.connect(self.re, SIGNAL('returnPressed()'), self.do_test)
self.re.setText(get_filename_pat())
self.re.setText(prefs['filename_pattern'])
def do_test(self):
try:
@ -66,9 +67,9 @@ class FilenamePattern(QWidget, Ui_Form):
return re.compile(pat)
def commit(self):
pat = self.pattern()
set_filename_pat(pat)
return pat.pattern
pat = self.pattern().pattern
prefs['filename_pattern'] = pat
return pat
@ -235,8 +236,10 @@ class JobsView(TableView):
def show_details(self, index):
row = index.row()
job = self.model().row_to_job(row)[0]
DetailView(self, job).exec_()
job = self.model().row_to_job(row)
d = DetailView(self, job)
self.connect(self.model(), SIGNAL('output_received()'), d.update)
d.exec_()
class FontFamilyModel(QAbstractListModel):
@ -365,13 +368,13 @@ class PythonHighlighter(QSyntaxHighlighter):
@classmethod
def loadConfig(cls):
Config = cls.Config
settings = QSettings()
def setDefaultString(name, default):
value = settings.value(name).toString()
if value.isEmpty():
value = default
Config[name] = value
settings = Settings()
for name in ("window", "shell"):
Config["%swidth" % name] = settings.value("%swidth" % name,
QVariant(QApplication.desktop() \

View File

@ -10,7 +10,8 @@ Command line interface to the calibre database.
import sys, os
from textwrap import TextWrapper
from calibre import OptionParser, Settings, terminal_controller, preferred_encoding
from calibre import terminal_controller, preferred_encoding
from calibre.utils.config import OptionParser, prefs
try:
from calibre.utils.single_qt_application import send_message
except:
@ -26,6 +27,7 @@ def get_parser(usage):
parser = OptionParser(usage)
go = parser.add_option_group('GLOBAL OPTIONS')
go.add_option('--library-path', default=None, help=_('Path to the calibre library. Default is to use the path stored in the settings.'))
return parser
def get_db(dbpath, options):
@ -44,7 +46,7 @@ def do_list(db, fields, sort_by, ascending, search_text):
widths = list(map(lambda x : 0, fields))
for i in db.data:
for j, field in enumerate(fields):
widths[j] = max(widths[j], len(unicode(i[field])))
widths[j] = max(widths[j], len(unicode(i[str(field)])))
screen_width = terminal_controller.COLS
if not screen_width:
@ -97,8 +99,7 @@ List the books available in the calibre database.
parser.add_option('-s', '--search', default=None,
help=_('Filter the results by the search query. For the format of the search query, please see the search related documentation in the User Manual. Default is to do no filtering.'))
opts, args = parser.parse_args(sys.argv[:1] + args)
fields = [f.strip().lower() for f in opts.fields.split(',')]
fields = [str(f.strip().lower()) for f in opts.fields.split(',')]
if not set(fields).issubset(FIELDS):
parser.print_help()
print
@ -424,7 +425,7 @@ For help on an individual command: %%prog command --help
return 1
command = eval('command_'+args[1])
dbpath = Settings().get('library path', os.path.expanduser('~'))
dbpath = prefs['library_path']
return command(args[2:], dbpath)

View File

@ -1413,7 +1413,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
mi.render(f)
f.close()
for fmt in self.formats(idx, index_is_id=index_is_id).split(','):
fmts = self.formats(idx, index_is_id=index_is_id)
if not fmts:
fmts = ''
for fmt in fmts.split(','):
data = self.format(idx, fmt, index_is_id=index_is_id)
fname = name +'.'+fmt.lower()
fname = sanitize_file_name(fname)

View File

@ -366,12 +366,14 @@ def install_man_pages(fatal_errors):
f.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
f.close()
manifest = []
os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
for src in entry_points['console_scripts']:
prog = src[:src.index('=')].strip()
if prog in ('prs500', 'pdf-meta', 'epub-meta', 'lit-meta',
'markdown-calibre', 'calibre-debug', 'fb2-meta',
'calibre-fontconfig', 'calibre-parallel'):
continue
help2man = ('help2man', prog, '--name', 'part of %s'%__appname__,
'--section', '1', '--no-info', '--include',
f.name, '--manual', __appname__)

View File

@ -244,6 +244,7 @@ def do_postinstall(destdir):
os.chdir(destdir)
os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '')
os.environ['PYTHONPATH'] = destdir
os.environ['PYTHONSTARTUP'] = ''
subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name))
finally:
os.chdir(cwd)

View File

@ -17,7 +17,7 @@ E-book Format Conversion
What formats does |app| support conversion to/from?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| supports the conversion of the following formats to LRF: HTML, LIT, MOBI, PRC, EPUB, RTF, TXT, PDF and LRS. It also supports the conversion of LRF to LRS and HTML. Note that calibre does not support the conversion of DRMed ebooks.
|app| supports the conversion of the following formats to LRF: HTML, LIT, MOBI, PRC, EPUB, CBR, CBZ, RTF, TXT, PDF and LRS. It also supports the conversion of LRF to LRS and HTML(forthcoming). Note that calibre does not support the conversion of DRMed ebooks.
What are the best formats to convert to LRF?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -142,7 +142,7 @@ If it still wont launch, start a command prompt (press the windows key and R; th
calibre-debug -c "from calibre.gui2.main import main; main()"
Post any output you see in a help message on the `Forums <http://calibre.kovidgoyal.net/discussion`_.
Post any output you see in a help message on the `Forums <http://calibre.kovidgoyal.net/discussion>`_.
I want some feature added to |app|. What can I do?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -27,7 +27,6 @@ is buffered and asynchronous to prevent the job from being IO bound.
import sys, os, gc, cPickle, traceback, atexit, cStringIO, time, signal, \
subprocess, socket, collections, binascii, re, thread, tempfile
from select import select
from functools import partial
from threading import RLock, Thread, Event
from calibre.ptempfile import PersistentTemporaryFile
@ -144,8 +143,10 @@ class WorkerMother(object):
self.prefix += 'import sys; sys.frameworks_dir = "%s"; sys.frozen = "macosx_app"; '%fd
self.prefix += 'sys.path.insert(0, %s); '%repr(sp)
if fd not in os.environ['PATH']:
self.env['PATH'] = os.environ['PATH']+':'+fd
self.env['PYTHONHOME'] = resources
self.env['PATH'] = os.environ['PATH']+':'+fd
self.env['PYTHONHOME'] = resources
self.env['MAGICK_HOME'] = os.path.join(getattr(sys, 'frameworks_dir'), 'ImageMagick')
self.env['DYLD_LIBRARY_PATH'] = os.path.join(getattr(sys, 'frameworks_dir'), 'ImageMagick', 'lib')
else:
self.executable = os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel') \
if isfrozen else 'calibre-parallel'
@ -332,7 +333,7 @@ class Overseer(object):
def __init__(self, server, port, timeout=5):
self.worker_status = mother.spawn_worker('127.0.0.1:'+str(port))
self.socket = server.accept()[0]
# Needed if terminate called hwen interpreter is shutting down
# Needed if terminate called when interpreter is shutting down
self.os = os
self.signal = signal
self.on_probation = False
@ -341,7 +342,6 @@ class Overseer(object):
self.working = False
self.timeout = timeout
self.last_job_time = time.time()
self.job_id = None
self._stop = False
if not select([self.socket], [], [], 120)[0]:
raise RuntimeError(_('Could not launch worker process.'))
@ -406,16 +406,14 @@ class Overseer(object):
`job`: An instance of :class:`Job`.
'''
self.job_id = job.job_id
self.working = True
self.write('JOB:'+cPickle.dumps((job.func, job.args, job.kwdargs), -1))
self.write('JOB:'+cPickle.dumps((job.func, job.args, job.kwargs), -1))
msg = self.read()
if msg != 'OK':
raise ControlError('Failed to initialize job on worker %d:%s'%(self.worker_pid, msg))
self.output = job.output if callable(job.output) else sys.stdout.write
self.progress = job.progress if callable(job.progress) else None
self.job = job
self.last_report = time.time()
job.start_work()
def control(self):
'''
@ -433,7 +431,9 @@ class Overseer(object):
else:
if self.on_probation:
self.terminate()
return Result(None, ControlError('Worker process died unexpectedly'), '')
self.job.result = None
self.job.exception = ControlError('Worker process died unexpectedly')
return
else:
self.on_probation = True
return
@ -443,13 +443,14 @@ class Overseer(object):
return
elif word == 'RESULT':
self.write('OK')
return Result(cPickle.loads(msg), None, None)
self.job.result = cPickle.loads(msg)
return True
elif word == 'OUTPUT':
self.write('OK')
try:
self.output(''.join(cPickle.loads(msg)))
self.job.output(''.join(cPickle.loads(msg)))
except:
self.output('Bad output message: '+ repr(msg))
self.job.output('Bad output message: '+ repr(msg))
elif word == 'PROGRESS':
self.write('OK')
percent = None
@ -457,45 +458,154 @@ class Overseer(object):
percent, msg = cPickle.loads(msg)[-1]
except:
print 'Bad progress update:', repr(msg)
if self.progress and percent is not None:
self.progress(percent, msg)
if percent is not None:
self.job.update_status(percent, msg)
elif word == 'ERROR':
self.write('OK')
return Result(None, *cPickle.loads(msg))
self.job.excetion, self.job.traceback = cPickle.loads(msg)
return True
else:
self.terminate()
return Result(None, ControlError('Worker sent invalid msg: %s'%repr(msg)), '')
self.job.exception = ControlError('Worker sent invalid msg: %s'%repr(msg))
return
if not self.worker_status.is_alive() or time.time() - self.last_report > 180:
self.terminate()
return Result(None, ControlError('Worker process died unexpectedly with returncode: %s'%str(self.process.returncode)), '')
self.job.exception = ControlError('Worker process died unexpectedly with returncode: %s'%str(self.process.returncode))
return
class JobKilled(Exception):
pass
class Job(object):
def __init__(self, job_id, func, args, kwdargs, output, progress, done):
self.job_id = job_id
def __init__(self, job_done, job_manager=None,
args=[], kwargs={}, description=None):
self.args = args
self.kwargs = kwargs
self._job_done = job_done
self.job_manager = job_manager
self.is_running = False
self.has_run = False
self.percent = -1
self.msg = None
self.description = description
self.start_time = None
self.running_time = None
self.result = self.exception = self.traceback = self.log = None
def __cmp__(self, other):
sstatus, ostatus = self.status(), other.status()
if sstatus == ostatus or (self.has_run and other.has_run):
if self.start_time == other.start_time:
return cmp(id(self), id(other))
return cmp(self.start_time, other.start_time)
if sstatus == 'WORKING':
return -1
if ostatus == 'WORKING':
return 1
if sstatus == 'WAITING':
return -1
if ostatus == 'WAITING':
return 1
def job_done(self):
self.is_running, self.has_run = False, True
self.running_time = (time.time() - self.start_time) if \
self.start_time is not None else 0
if self.job_manager is not None:
self.job_manager.job_done(self)
self._job_done(self)
def start_work(self):
self.is_running = True
self.has_run = False
self.start_time = time.time()
if self.job_manager is not None:
self.job_manager.start_work(self)
def update_status(self, percent, msg=None):
self.percent = percent
self.msg = msg
if self.job_manager is not None:
self.job_manager.status_update(self)
def status(self):
if self.is_running:
return 'WORKING'
if not self.has_run:
return 'WAITING'
if self.has_run:
if self.exception is None:
return 'DONE'
return 'ERROR'
def console_text(self):
ans = [u'Error in job: ']
if self.description:
ans[0] += self.description
if self.log:
if isinstance(self.log, str):
self.log = unicode(self.log, 'utf-8', 'replace')
ans.append(self.log)
header = unicode(self.exception.__class__.__name__) if \
hasattr(self.exception, '__class__') else u'Error'
header += u': '
try:
header += unicode(self.exception)
except:
header += unicode(repr(self.exception))
ans.append(header)
if self.traceback:
ans.append(self.traceback)
return (u'\n'.join(ans)).encode('utf-8')
def gui_text(self):
ans = [u'Job: ']
if self.description:
if not isinstance(self.description, unicode):
self.description = self.description.decode('utf-8', 'replace')
ans[0] += u'<b>%s</b>'%self.description
if self.exception is not None:
header = unicode(self.exception.__class__.__name__) if \
hasattr(self.exception, '__class__') else u'Error'
header = u'<b>%s</b>'%header
header += u': '
try:
header += unicode(self.exception)
except:
header += unicode(repr(self.exception))
ans.append(header)
if self.traceback:
ans.append(u'<b>Traceback</b>:')
ans.extend(self.traceback.split('\n'))
if self.log:
ans.append(u'<b>Log</b>:')
if isinstance(self.log, str):
self.log = unicode(self.log, 'utf-8', 'replace')
ans.extend(self.log.split('\n'))
return '<br>'.join(ans)
class ParallelJob(Job):
def __init__(self, func, *args, **kwargs):
Job.__init__(self, *args, **kwargs)
self.func = func
self.args = args
self.kwdargs = kwdargs
self.output = output
self.progress = progress
self.done = done
self.done = self.job_done
class Result(object):
def output(self, msg):
if not self.log:
self.log = u''
if not isinstance(msg, unicode):
msg = msg.decode('utf-8', 'replace')
if msg:
self.log += msg
if self.job_manager is not None:
self.job_manager.output(self)
def __init__(self, result, exception, traceback):
self.result = result
self.exception = exception
self.traceback = traceback
def __len__(self):
return 3
def __item__(self, i):
return (self.result, self.exception, self.traceback)[i]
def __iter__(self):
return iter((self.result, self.exception, self.traceback))
def remove_ipc_socket(path):
os = __import__('os')
@ -525,7 +635,7 @@ class Server(Thread):
atexit.register(remove_ipc_socket, self.port)
self.server_socket.listen(5)
self.number_of_workers = number_of_workers
self.pool, self.jobs, self.working, self.results = [], collections.deque(), [], {}
self.pool, self.jobs, self.working = [], collections.deque(), []
atexit.register(self.killall)
atexit.register(self.close)
self.job_lock = RLock()
@ -544,15 +654,8 @@ class Server(Thread):
def add_job(self, job):
with self.job_lock:
self.jobs.append(job)
def store_result(self, result, id=None):
if id:
with self.job_lock:
self.results[id] = result
def result(self, id):
with self.result_lock:
return self.results.pop(id, None)
if job.job_manager is not None:
job.job_manager.add_job(job)
def run(self):
while True:
@ -575,8 +678,9 @@ class Server(Thread):
o.initialize_job(job)
except Exception, err:
o.terminate()
res = Result(None, unicode(err), traceback.format_exc())
job.done(res)
job.exception = err
job.traceback = traceback.format_exc()
job.done()
o = None
if o and o.is_viable():
with self.working_lock:
@ -586,12 +690,14 @@ class Server(Thread):
done = []
for o in self.working:
try:
res = o.control()
if o.control() is not None or o.job.exception is not None:
o.job.done()
done.append(o)
except Exception, err:
res = Result(None, unicode(err), traceback.format_exc())
o.job.exception = err
o.job.traceback = traceback.format_exc()
o.terminate()
if isinstance(res, Result):
o.job.done(res)
o.job.done()
done.append(o)
for o in done:
self.working.remove(o)
@ -611,32 +717,23 @@ class Server(Thread):
self.pool = []
def kill(self, job_id):
def kill(self, job):
with self.working_lock:
pop = None
for o in self.working:
if o.job_id == job_id:
o.terminate()
o.job.done(Result(self.KILL_RESULT, None, ''))
if o.job == job or o == job:
try:
o.terminate()
except: pass
o.job.exception = JobKilled(_('Job stopped by user'))
try:
o.job.done()
except: pass
pop = o
break
if pop is not None:
self.working.remove(pop)
def run_job(self, job_id, func, args=[], kwdargs={},
output=None, progress=None, done=None):
'''
Run a job in a separate process. Supports job control, output redirection
and progress reporting.
'''
if done is None:
done = partial(self.store_result, id=job_id)
job = Job(job_id, func, args, kwdargs, output, progress, done)
with self.job_lock:
self.jobs.append(job)
def run_free_job(self, func, args=[], kwdargs={}):
pt = PersistentTemporaryFile('.pickle', '_IPC_')
pt.write(cPickle.dumps((func, args, kwdargs)))

149
src/calibre/startup.py Normal file
View File

@ -0,0 +1,149 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Perform various initialization tasks.
'''
import locale, sys, os, re, cStringIO
from gettext import GNUTranslations
# Default translation is NOOP
import __builtin__
__builtin__.__dict__['_'] = lambda s: s
from calibre.constants import iswindows, isosx, islinux, isfrozen
from calibre.translations.msgfmt import make
_run_once = False
if not _run_once:
_run_once = True
################################################################################
# Setup translations
def get_lang():
lang = locale.getdefaultlocale()[0]
if lang is None and os.environ.has_key('LANG'): # Needed for OS X
try:
lang = os.environ['LANG']
except:
pass
if lang:
match = re.match('[a-z]{2,3}', lang)
if match:
lang = match.group()
return lang
def set_translator():
# To test different translations invoke as
# LC_ALL=de_DE.utf8 program
try:
from calibre.translations.compiled import translations
except:
return
lang = get_lang()
if lang:
buf = None
if os.access(lang+'.po', os.R_OK):
buf = cStringIO.StringIO()
make(lang+'.po', buf)
buf = cStringIO.StringIO(buf.getvalue())
elif translations.has_key(lang):
buf = cStringIO.StringIO(translations[lang])
if buf is not None:
t = GNUTranslations(buf)
t.install(unicode=True)
set_translator()
################################################################################
# Initialize locale
try:
locale.setlocale(locale.LC_ALL, '')
except:
dl = locale.getdefaultlocale()
try:
if dl:
locale.setlocale(dl[0])
except:
pass
################################################################################
# Load plugins
if isfrozen:
if iswindows:
plugin_path = os.path.join(os.path.dirname(sys.executable), 'plugins')
sys.path.insert(1, os.path.dirname(sys.executable))
elif isosx:
plugin_path = os.path.join(getattr(sys, 'frameworks_dir'), 'plugins')
elif islinux:
plugin_path = os.path.join(getattr(sys, 'frozen_path'), 'plugins')
sys.path.insert(0, plugin_path)
else:
import pkg_resources
plugins = getattr(pkg_resources, 'resource_filename')('calibre', 'plugins')
sys.path.insert(0, plugins)
plugins = {}
for plugin in ['pictureflow', 'lzx', 'msdes'] + \
(['winutil'] if iswindows else []) + \
(['usbobserver'] if isosx else []):
try:
p, err = __import__(plugin), ''
except Exception, err:
p = None
err = str(err)
plugins[plugin] = (p, err)
################################################################################
# Improve builtin path functions to handle unicode sensibly
_abspath = os.path.abspath
def my_abspath(path, encoding=sys.getfilesystemencoding()):
'''
Work around for buggy os.path.abspath. This function accepts either byte strings,
in which it calls os.path.abspath, or unicode string, in which case it first converts
to byte strings using `encoding`, calls abspath and then decodes back to unicode.
'''
to_unicode = False
if isinstance(path, unicode):
path = path.encode(encoding)
to_unicode = True
res = _abspath(path)
if to_unicode:
res = res.decode(encoding)
return res
os.path.abspath = my_abspath
_join = os.path.join
def my_join(a, *p):
encoding=sys.getfilesystemencoding()
p = [a] + list(p)
_unicode = False
for i in p:
if isinstance(i, unicode):
_unicode = True
break
p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p]
res = _join(*p)
if _unicode:
res = res.decode(encoding)
return res
os.path.join = my_join
################################################################################
# Platform specific modules
winutil = winutilerror = None
if iswindows:
winutil, winutilerror = plugins['winutil']
if not winutil:
raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror)
if len(sys.argv) > 1:
sys.argv[1:] = winutil.argv()[1-len(sys.argv):]
################################################################################

View File

@ -35,7 +35,6 @@ class Distribution(object):
('ImageMagick', '6.3.5', 'imagemagick', 'imagemagick', 'ImageMagick'),
('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'),
('dbus-python', '0.82.2', 'dbus-python', 'python-dbus', 'dbus-python'),
('convertlit', '1.8', 'convertlit', None, None),
('lxml', '1.3.3', 'lxml', 'python-lxml', 'python-lxml'),
('help2man', '1.36.4', 'help2man', 'help2man', 'help2man'),
]
@ -49,10 +48,7 @@ class Distribution(object):
'fedora':'Fedora 8', 'debian':'Debian Sid', 'generic': 'Generic Unix'}
MANUAL_MAP = {
'ubuntu' : '<li>You will have to install <a href="">convertlit</a> manually to be able to convert LIT files.</li>',
'fedora' : '''<li>You have to upgrade Qt to at least 4.3.1 and PyQt to at least 4.3.1</li>'''\
'''<li>You will have to install <a href="">convertlit</a> manually to be able to convert LIT files.</li>''',
'debian' : '<li>Add the following to /etc/apt/sources.list<pre class="wiki">deb http://www.debian-multimedia.org sid main</pre>Then<pre class="wiki">apt-get install clit</pre></li>',
'fedora' : '''<li>You have to upgrade Qt to at least 4.4.0 and PyQt to at least 4.4.2</li>''',
}
def __init__(self, os):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -164,8 +164,8 @@ DEFAULTKEYWORDS = ', '.join(default_keywords)
EMPTYSTRING = ''
from calibre import __appname__
from calibre import __version__ as version
from calibre.constants import __appname__
from calibre.constants import __version__ as version
# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
# there.

View File

@ -6,36 +6,54 @@ msgid ""
msgstr ""
"Project-Id-Version: calibre 0.4.55\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-07-21 22:18+0000\n"
"PO-Revision-Date: 2008-07-22 05:27+0000\n"
"POT-Creation-Date: 2008-08-03 09:01+0000\n"
"PO-Revision-Date: 2008-07-22 05:50+0000\n"
"Last-Translator: Kovid Goyal <Unknown>\n"
"Language-Team: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2008-07-22 05:44+0000\n"
"X-Launchpad-Export-Date: 2008-08-04 16:52+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\n"
#: /home/kovid/work/calibre/src/calibre/__init__.py:138
#: /home/kovid/work/calibre/src/calibre/__init__.py:178
#, fuzzy
msgid "%sUsage%s: %s\n"
msgstr "%sИспользовано%s: %s\n"
#: /home/kovid/work/calibre/src/calibre/__init__.py:175
#: /home/kovid/work/calibre/src/calibre/__init__.py:215
msgid "Created by "
msgstr "Сделано "
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:112
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:146
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:174
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:113
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:147
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:175
msgid "Unable to detect the %s disk drive. Try rebooting."
msgstr "Не удалось определить диск %s. Попробуйте перезагрузиться."
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:355
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:356
msgid "The reader has no storage card connected."
msgstr "К ридеру не подключена карта памяти."
#: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:780
msgid "%prog [options] LITFILE"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:783
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:404
msgid "Output directory. Defaults to current directory."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:786
msgid "Useful for debugging."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lit/reader.py:797
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:425
msgid "OEB ebook created in"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:71
msgid "Set the title. Default: filename."
msgstr "Укажите заголовок. По умолчанию: имя файла."
@ -50,14 +68,17 @@ msgstr ""
"умолчанию: %default"
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:74
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:239
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/__init__.py:174
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf.py:314
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf.py:429
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:52
#: /home/kovid/work/calibre/src/calibre/gui2/library.py:278
#: /home/kovid/work/calibre/src/calibre/gui2/library.py:685
#: /home/kovid/work/calibre/src/calibre/library/database.py:879
#: /home/kovid/work/calibre/src/calibre/library/database.py:1387
#: /home/kovid/work/calibre/src/calibre/library/database.py:1517
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:926
#: /home/kovid/work/calibre/src/calibre/library/database.py:904
#: /home/kovid/work/calibre/src/calibre/library/database.py:1412
#: /home/kovid/work/calibre/src/calibre/library/database.py:1542
msgid "Unknown"
msgstr "Неизвестно"
@ -414,6 +435,88 @@ msgstr ""
msgid "No file to convert specified."
msgstr "Не указан файл для преобразования."
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:200
msgid "Rendered %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:230
msgid ""
"Options to control the conversion of comics (CBR, CBZ) files into ebooks"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:236
msgid "Title for generated ebook. Default is to use the filename."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:238
msgid ""
"Set the author in the metadata of the generated ebook. Default is %default"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:241
msgid ""
"Path to output LRF file. By default a file is created in the current "
"directory."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:243
msgid "Number of colors for Grayscale image conversion. Default: %default"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:245
msgid ""
"Disable normalize (improve contrast) color range for pictures. Default: False"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:247
msgid "Maintain picture aspect ratio. Default is to fill the screen."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:249
msgid "Disable sharpening."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:251
msgid "Don't split landscape images into two portrait images"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:253
msgid ""
"Don't sort the files found in the comic alphabetically by name. Instead use "
"the order they were added to the comic."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:255
msgid ""
"Choose a profile for the device you are generating this LRF for. The default "
"is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:257
msgid ""
"Be verbose, useful for debugging. Can be specified multiple times for "
"greater verbosity."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:259
msgid "Don't show progress bar."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:264
msgid ""
"%prog [options] comic.cb[z|r]\n"
"\n"
"Convert a comic in a CBZ or CBR file to an LRF ebook. \n"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:328
msgid "Rendering comic pages..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/comic/convert_from.py:334
msgid "Output written to"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/epub/convert_from.py:17
msgid ""
"Usage: %prog [options] mybook.epub\n"
@ -515,40 +618,40 @@ msgid ""
"%s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1749
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1752
msgid ""
"An error occurred while processing a table: %s. Ignoring table markup."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1751
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1754
msgid ""
"Bad table:\n"
"%s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1773
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1776
msgid "Table has cell that is too large"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1803
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1806
msgid ""
"You have to save the website %s as an html file first and then run html2lrf "
"on it."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1846
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1849
msgid "Could not read cover image: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1849
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1852
msgid "Cannot read from: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1984
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1987
msgid "Failed to process opf file"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1990
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/html/convert_from.py:1993
msgid ""
"Usage: %prog [options] mybook.html\n"
"\n"
@ -559,7 +662,7 @@ msgid ""
"convert a whole tree of HTML files."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/lit/convert_from.py:21
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/lit/convert_from.py:22
msgid ""
"Usage: %prog [options] mybook.lit\n"
"\n"
@ -741,11 +844,10 @@ msgstr ""
msgid "Set the comment"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:100
msgid "mybook.epub"
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:117
msgid "A comma separated list of tags to set"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:100
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/fb2.py:50
msgid "Usage:"
msgstr ""
@ -803,11 +905,11 @@ msgid ""
"Fetch a cover image for the book identified by ISBN from LibraryThing.com\n"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/lit.py:751
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/lit.py:40
msgid "Usage: %s file.lit"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/lit.py:758
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/lit.py:50
msgid "Cover saved to"
msgstr ""
@ -819,22 +921,14 @@ msgstr ""
msgid "No filename specified."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:391
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:402
msgid "%prog [options] myebook.mobi"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:393
msgid "Output directory. Defaults to current directory."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:412
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:423
msgid "Raw MOBI HTML saved in"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:414
msgid "OEB ebook created in"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:22
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:26
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:36
@ -855,6 +949,7 @@ msgid "Comments"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:55
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:79
msgid "Dialog"
msgstr ""
@ -869,6 +964,51 @@ msgstr ""
msgid "Choose Format"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:38
msgid "Set defaults for conversion of comics (CBR/CBZ files)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:53
msgid "Set options for converting %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:80
msgid "&Title:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:81
msgid "&Author(s):"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:82
msgid "&Number of Colors:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:83
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single_ui.py:552
msgid "&Profile:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:84
msgid "Disable &normalize"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:85
msgid "Keep &aspect ratio"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:86
msgid "Disable &Sharpening"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:87
msgid "&Landscape"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf_ui.py:88
msgid "Dont so&rt"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:23
msgid "Basic"
msgstr ""
@ -877,47 +1017,47 @@ msgstr ""
msgid "Advanced"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:105
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:107
msgid "<br>Must be a directory."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:105
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:107
msgid "Invalid database location "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:105
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:108
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:107
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:110
msgid "Invalid database location"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:108
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:110
msgid "Invalid database location.<br>Cannot write to "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:122
msgid "Compacting database. This may take a while."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:120
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:122
msgid "Compacting..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:216
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219
#: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:265
msgid "Configuration"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:217
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:220
msgid "&Location of books database (library1.db)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:218
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:221
msgid "Browse for the new database location"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:237
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:239
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:222
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:241
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:243
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single_ui.py:513
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:266
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:285
@ -937,89 +1077,93 @@ msgstr ""
msgid "..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:220
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:223
msgid "Use &Roman numerals for series number"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:221
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:224
msgid "&Number of covers to show in browse mode (after restart):"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:222
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:225
msgid "Show notification when &new version is available"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:223
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:226
msgid "Ask for &confirmation before deleting files"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:227
msgid "Format for &single file save:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:224
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:228
msgid "&Priority for conversion jobs:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:225
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:229
msgid "Default network &timeout:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:226
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:230
msgid ""
"Set the default timeout for network fetches (i.e. anytime we go out to the "
"internet to get information)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:227
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:231
msgid " seconds"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:228
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:232
msgid "Toolbar"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:229
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:233
msgid "Large"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:230
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:234
msgid "Medium"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:231
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:235
msgid "Small"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:232
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:236
msgid "&Button size in toolbar"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:233
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:237
msgid "Show &text in toolbar buttons"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:234
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:238
msgid "Select visible &columns in library view"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:235
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:239
msgid "Frequently used directories"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:236
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:240
msgid "Add a directory to the frequently used directories list"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:238
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:242
msgid "Remove a directory from the frequently used directories list"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:240
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:244
msgid "Free unused diskspace from the database"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:241
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:245
msgid "&Compact database"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:242
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:246
msgid "&Metadata from file name"
msgstr ""
@ -1151,7 +1295,6 @@ msgid "Convert %s to LRF"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:108
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:177
msgid "Set conversion defaults"
msgstr ""
@ -1415,10 +1558,6 @@ msgstr ""
msgid "Override<br>CSS"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single_ui.py:552
msgid "&Profile:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single_ui.py:553
msgid "&Left Margin:"
msgstr ""
@ -2261,23 +2400,31 @@ msgstr ""
msgid "Bulk convert"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:309
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:177
msgid "Set defaults for conversion to LRF"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:178
msgid "Set defaults for conversion of comics"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:311
msgid " detected."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:309
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:311
msgid "Device: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:334
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:336
msgid "Connected "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:346
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:348
msgid "Device database corrupted"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:347
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:349
msgid ""
"\n"
" <p>The database of books on the reader is corrupted. Try the "
@ -2293,190 +2440,222 @@ msgid ""
" "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:399
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:473
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:401
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:475
msgid ""
"<p>Books with the same title as the following already exist in the database. "
"Add them anyway?<ul>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:402
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:476
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:404
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:478
msgid "Duplicates found!"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:435
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:448
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:437
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:450
msgid "Uploading books to device."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:507
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:509
msgid "No space on device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:508
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:510
msgid ""
"<p>Cannot upload books to device there is no more free space available "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:546
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:541
msgid "Confirm delete"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:542
msgid "Are you sure you want to delete these %d books?"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:554
msgid "Deleting books from device."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:578
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:599
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:586
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:607
msgid "Cannot edit metadata"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:578
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:599
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:694
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:764
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:834
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:586
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:607
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:702
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:772
msgid "No books selected"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:674
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:682
msgid "Sending books to device."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:677
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:685
msgid "No suitable formats"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:678
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:686
msgid ""
"Could not upload the following books to the device, as no suitable formats "
"were found:<br><ul>%s</ul>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:694
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:702
msgid "Cannot save to disk"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:705
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:713
msgid ""
"<p>Could not save the following books to disk, because the %s format is not "
"available for them:<ul>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:709
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:717
msgid "Could not save some ebooks"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:742
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:750
msgid "Fetch news from "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:744
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:752
msgid "Fetching news from "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:754
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:762
msgid "News fetched. Uploading to device."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:764
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:834
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:772
msgid "Cannot convert"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:773
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:794
msgid "Starting Bulk conversion of %d books"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:905
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:923
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:832
msgid "Convert book %d of %d (%s)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:842
msgid ""
"<p>Could not convert %d of %d books, because no suitable source format was "
"found.<ul>%s</ul>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:843
msgid "Could not convert some books"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:878
msgid "Convert comic %d of %d (%s)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:908
msgid "Convert book: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:953
msgid "Convert comic: "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1001
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1019
msgid "No book selected"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:905
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:923
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:937
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1001
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1019
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1033
msgid "Cannot view"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:911
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:942
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1007
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1038
msgid "Choose the format to view"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:938
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1034
msgid "%s has no available formats."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:976
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1072
msgid "Cannot configure"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:976
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1072
msgid "Cannot configure while there are running jobs."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:999
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1095
msgid "Copying database to "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1014
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1110
msgid "Invalid database"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1015
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1111
msgid ""
"<p>An invalid database already exists at %s, delete it before trying to move "
"the existing database.<br>Error: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1023
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1119
msgid "Could not move database"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1044
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1140
msgid "No detailed info available"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1045
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1141
msgid "No detailed information is available for books on the device."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1087
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1183
msgid "Error talking to device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1088
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1184
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/main.py:1139
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1235
msgid "Conversion Error"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1158
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1254
msgid "Database does not exist"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1158
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1254
msgid ""
"The directory in which the database should be: %s no longer exists. Please "
"choose a new database location."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1209
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1305
msgid ""
"<span style=\"color:red; font-weight:bold\">Latest version: <a "
"href=\"%s\">%s</a></span>"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1215
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1311
msgid ""
"%s has been updated to version %s. See the <a "
"href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features</a>. "
"Visit the download page?"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1215
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1311
msgid "Update available"
msgstr ""
@ -2589,23 +2768,23 @@ msgstr ""
msgid "Custom news sources"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:95
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:99
msgid "Jobs:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:104
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:108
msgid "Click to see list of active jobs."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:133
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:137
msgid "Click to browse books by their covers"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:133
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:137
msgid "Click to turn off Cover Browsing"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:138
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:142
msgid ""
"<p>Browsing books by their covers is disabled.<br>Import of pictureflow "
"module failed:<br>"
@ -2851,11 +3030,11 @@ msgid ""
"For help on an individual command: %%prog command --help\n"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/parallel.py:344
#: /home/kovid/work/calibre/src/calibre/parallel.py:347
msgid "Could not launch worker process."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/utils/fontconfig.py:157
#: /home/kovid/work/calibre/src/calibre/utils/fontconfig.py:169
msgid "Could not initialize the fontconfig library"
msgstr ""
@ -2964,9 +3143,8 @@ msgid ""
"downloads at most 2 feeds."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:84
#: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:88
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:577
#: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:70
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:580
msgid "Fetching feeds..."
msgstr ""
@ -2995,58 +3173,58 @@ msgstr ""
msgid "\tFailed links:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:559
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:562
msgid "Could not fetch article. Run with --debug to see the reason"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:581
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:584
msgid "Got feeds from index page"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:585
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:588
msgid "Trying to download cover..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:637
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:640
msgid "Starting download [%d thread(s)]..."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:650
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:653
msgid "Feeds downloaded to %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:660
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:663
msgid "Could not download cover: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:665
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:668
msgid "Downloading cover from %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:699
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:702
msgid "Untitled Article"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:745
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:748
msgid ""
"\n"
"Downloaded article %s from %s\n"
"%s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:751
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:754
msgid "Article downloaded: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:757
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:760
msgid "Failed to download article: %s from %s\n"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:762
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:765
msgid "Article download failed: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:777
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:780
msgid "Fetching feed"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,14 @@ __docformat__ = 'restructuredtext en'
'''
Manage application-wide preferences.
'''
import os, re, cPickle
import os, re, cPickle, textwrap
from copy import deepcopy
from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from PyQt4.QtCore import QString
from calibre import iswindows, isosx, OptionParser, ExclusiveFile, LockError
from calibre.constants import terminal_controller, iswindows, isosx, \
__appname__, __version__, __author__
from calibre.utils.lock import LockError, ExclusiveFile
from collections import defaultdict
if iswindows:
@ -21,10 +25,129 @@ if iswindows:
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
config_dir = os.path.expanduser('~/.config/calibre')
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
if not os.path.exists(config_dir):
os.makedirs(config_dir)
os.makedirs(config_dir, mode=448) # 0700 == 448
class CustomHelpFormatter(IndentedHelpFormatter):
def format_usage(self, usage):
return _("%sUsage%s: %s\n") % (terminal_controller.BLUE, terminal_controller.NORMAL, usage)
def format_heading(self, heading):
return "%*s%s%s%s:\n" % (self.current_indent, terminal_controller.BLUE,
"", heading, terminal_controller.NORMAL)
def format_option(self, option):
result = []
opts = self.option_strings[option]
opt_width = self.help_position - self.current_indent - 2
if len(opts) > opt_width:
opts = "%*s%s\n" % (self.current_indent, "",
terminal_controller.GREEN+opts+terminal_controller.NORMAL)
indent_first = self.help_position
else: # start help on same line as opts
opts = "%*s%-*s " % (self.current_indent, "", opt_width + len(terminal_controller.GREEN + terminal_controller.NORMAL),
terminal_controller.GREEN + opts + terminal_controller.NORMAL)
indent_first = 0
result.append(opts)
if option.help:
help_text = self.expand_default(option).split('\n')
help_lines = []
for line in help_text:
help_lines.extend(textwrap.wrap(line, self.help_width))
result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
result.extend(["%*s%s\n" % (self.help_position, "", line)
for line in help_lines[1:]])
elif opts[-1] != "\n":
result.append("\n")
return "".join(result)+'\n'
class OptionParser(_OptionParser):
def __init__(self,
usage='%prog [options] filename',
version='%%prog (%s %s)'%(__appname__, __version__),
epilog=_('Created by ')+terminal_controller.RED+__author__+terminal_controller.NORMAL,
gui_mode=False,
conflict_handler='resolve',
**kwds):
usage += '''\n\nWhenever you pass arguments to %prog that have spaces in them, '''\
'''enclose the arguments in quotation marks.'''
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
formatter=CustomHelpFormatter(),
conflict_handler=conflict_handler, **kwds)
self.gui_mode = gui_mode
def error(self, msg):
if self.gui_mode:
raise Exception(msg)
_OptionParser.error(self, msg)
def merge(self, parser):
'''
Add options from parser to self. In case of conflicts, confilicting options from
parser are skipped.
'''
opts = list(parser.option_list)
groups = list(parser.option_groups)
def merge_options(options, container):
for opt in deepcopy(options):
if not self.has_option(opt.get_opt_string()):
container.add_option(opt)
merge_options(opts, self)
for group in groups:
g = self.add_option_group(group.title)
merge_options(group.option_list, g)
def subsume(self, group_name, msg=''):
'''
Move all existing options into a subgroup named
C{group_name} with description C{msg}.
'''
opts = [opt for opt in self.options_iter() if opt.get_opt_string() not in ('--version', '--help')]
self.option_groups = []
subgroup = self.add_option_group(group_name, msg)
for opt in opts:
self.remove_option(opt.get_opt_string())
subgroup.add_option(opt)
def options_iter(self):
for opt in self.option_list:
if str(opt).strip():
yield opt
for gr in self.option_groups:
for opt in gr.option_list:
if str(opt).strip():
yield opt
def option_by_dest(self, dest):
for opt in self.options_iter():
if opt.dest == dest:
return opt
def merge_options(self, lower, upper):
'''
Merge options in lower and upper option lists into upper.
Default values in upper are overridden by
non default values in lower.
'''
for dest in lower.__dict__.keys():
if not upper.__dict__.has_key(dest):
continue
opt = self.option_by_dest(dest)
if lower.__dict__[dest] != opt.default and \
upper.__dict__[dest] == opt.default:
upper.__dict__[dest] = lower.__dict__[dest]
class Option(object):
@ -202,6 +325,15 @@ class Config(object):
raise IOError('Could not lock config file: %s'%self.config_file_path)
return self.option_set.parse_string(src)
def as_string(self):
if not os.path.exists(self.config_file_path):
return ''
try:
with ExclusiveFile(self.config_file_path) as f:
return f.read()
except LockError:
raise IOError('Could not lock config file: %s'%self.config_file_path)
def set(self, name, val):
if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%name)
@ -241,7 +373,153 @@ class StringConfig(object):
footer = self.option_set.get_override_section(self.src)
self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n'
class ConfigProxy(object):
'''
A Proxy to minimize file reads for widely used config settings
'''
def __init__(self, config):
self.__config = config
self.__opts = None
def refresh(self):
self.__opts = self.__config.parse()
def __getitem__(self, key):
return self.get(key)
def __setitem__(self, key, val):
return self.set(key, val)
def get(self, key):
if self.__opts is None:
self.refresh()
return getattr(self.__opts, key)
def set(self, key, val):
if self.__opts is None:
self.refresh()
setattr(self.__opts, key, val)
return self.__config.set(key, val)
class DynamicConfig(dict):
'''
A replacement for QSettings that supports dynamic config keys.
Returns `None` if a config key is not found. Note that the config
data is stored in a non human readable pickle file, so only use this
class for preferences that you don't intend to have the users edit directly.
'''
def __init__(self, name='dynamic'):
self.name = name
self.file_path = os.path.join(config_dir, name+'.pickle')
with ExclusiveFile(self.file_path) as f:
raw = f.read()
d = cPickle.loads(raw) if raw.strip() else {}
dict.__init__(self, d)
def __getitem__(self, key):
try:
return dict.__getitem__(self, key)
except KeyError:
return None
def __setitem__(self, key, val):
dict.__setitem__(self, key, val)
self.commit()
def set(self, key, val):
self.__setitem__(key, val)
def commit(self):
if hasattr(self, 'file_path') and self.file_path:
with ExclusiveFile(self.file_path) as f:
raw = cPickle.dumps(self, -1)
f.seek(0)
f.truncate()
f.write(raw)
dynamic = DynamicConfig()
def _prefs():
c = Config('global', 'calibre wide preferences')
c.add_opt('database_path',
default=os.path.expanduser('~/library1.db'),
help=_('Path to the database in which books are stored'))
c.add_opt('filename_pattern', default=ur'(?P<title>.+) - (?P<author>[^_]+)',
help=_('Pattern to guess metadata from filenames'))
c.add_opt('isbndb_com_key', default='',
help=_('Access key for isbndb.com'))
c.add_opt('network_timeout', default=5,
help=_('Default timeout for network operations (seconds)'))
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
return c
prefs = ConfigProxy(_prefs())
def migrate():
p = prefs
if p.get('migrated'):
return
from PyQt4.QtCore import QSettings, QVariant
class Settings(QSettings):
def __init__(self, name='calibre2'):
QSettings.__init__(self, QSettings.IniFormat, QSettings.UserScope,
'kovidgoyal.net', name)
def get(self, key, default=None):
try:
key = str(key)
if not self.contains(key):
return default
val = str(self.value(key, QVariant()).toByteArray())
if not val:
return None
return cPickle.loads(val)
except:
return default
s, migrated = Settings(), set([])
all_keys = set(map(unicode, s.allKeys()))
from calibre.gui2 import config, dynamic
def _migrate(key, safe=None, from_qvariant=None, p=config):
try:
if key not in all_keys:
return
if safe is None:
safe = re.sub(r'[^0-9a-zA-Z]', '_', key)
val = s.get(key)
if from_qvariant is not None:
val = getattr(s.value(key), from_qvariant)()
p.set(safe, val)
except:
pass
finally:
migrated.add(key)
_migrate('database path', p=prefs)
_migrate('filename pattern', p=prefs)
_migrate('network timeout', p=prefs)
_migrate('isbndb.com key', p=prefs)
_migrate('frequently used directories')
_migrate('send to device by default')
_migrate('save to disk single format')
_migrate('confirm delete')
_migrate('show text in toolbar')
_migrate('new version notification')
_migrate('use roman numerals for series number')
_migrate('cover flow queue length')
_migrate('LRF conversion defaults')
_migrate('LRF ebook viewer options')
for key in all_keys - migrated:
if key.endswith(': un') or key.endswith(': pw'):
_migrate(key, p=dynamic)
p.set('migrated', True)
if __name__ == '__main__':

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
'''
Make strings safe for use as ASCII filenames, while trying to preserve as much
meaning as possible.
'''
import re, string
MAP = {
u"" : u"'",
u"" : u"'",
u"«" : u'"',
u"»" : u'"',
u"" : u"...",
u"" : u"#",
u"Щ" : u"Sch",
u"Щ" : u"SCH",
u"Ё" : u"Yo",
u"Ё" : u"YO",
u"Ж" : u"Zh",
u"Ж" : u"ZH",
u"Ц" : u"Ts",
u"Ц" : u"TS",
u"Ч" : u"Ch",
u"Ч" : u"CH",
u"Ш" : u"Sh",
u"Ш" : u"SH",
u"Ы" : u"Yi",
u"Ы" : u"YI",
u"Ю" : u"Yu",
u"Ю" : u"YU",
u"Я" : u"Ya",
u"Я" : u"YA",
u"Б" : u"B",
u"Г" : u"G",
u"Д" : u"D",
u"И" : u"I",
u"Й" : u"J",
u"К" : u"K",
u"Л" : u"L",
u"П" : u"P",
u"Ф" : u"F",
u"Э" : u"E",
u"Ъ" : u"`",
u"Ь" : u"'",
u"щ" : u"sch",
u"ё" : u"yo",
u"ж" : u"zh",
u"ц" : u"ts",
u"ч" : u"ch",
u"ш" : u"sh",
u"ы" : u"yi",
u"ю" : u"yu",
u"я" : u"ya",
u"б" : u"b",
u"в" : u"v",
u"г" : u"g",
u"д" : u"d",
u"з" : u"z",
u"и" : u"i",
u"й" : u"j",
u"к" : u"k",
u"л" : u"l",
u"м" : u"m",
u"н" : u"n",
u"о" : u"o",
u"п" : u"p",
u"т" : u"t",
u"ф" : u"f",
u"э" : u"e",
u"ъ" : u"`",
u"ь" : u"'",
} #: Translation table
def ascii_filename(orig):
orig = PAT.sub(lambda m:MAP[m.group()], orig)
buf = []
for i in range(len(orig)):
val = ord(orig[i])
buf.append('_' if val < 33 or val > 126 else orig[i])
return (''.join(buf)).encode('ascii')

86
src/calibre/utils/lock.py Normal file
View File

@ -0,0 +1,86 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Secure access to locked files from multiple processes.
'''
from calibre.constants import iswindows, __appname__, \
win32api, win32event, winerror, fcntl
import time, atexit, os
class LockError(Exception):
pass
class ExclusiveFile(object):
def __init__(self, path, timeout=10):
self.path = path
self.timeout = timeout
def __enter__(self):
self.file = open(self.path, 'a+b')
self.file.seek(0)
timeout = self.timeout
if iswindows:
name = ('Local\\'+(__appname__+self.file.name).replace('\\', '_'))[:201]
while self.timeout < 0 or timeout >= 0:
self.mutex = win32event.CreateMutex(None, False, name)
if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: break
time.sleep(1)
timeout -= 1
else:
while self.timeout < 0 or timeout >= 0:
try:
fcntl.lockf(self.file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
break
except IOError:
time.sleep(1)
timeout -= 1
if timeout < 0 and self.timeout >= 0:
self.file.close()
raise LockError
return self.file
def __exit__(self, type, value, traceback):
self.file.close()
if iswindows:
win32api.CloseHandle(self.mutex)
def _clean_lock_file(file):
try:
file.close()
except:
pass
try:
os.remove(file.name)
except:
pass
def singleinstance(name):
'''
Return True if no other instance of the application identified by name is running,
False otherwise.
@param name: The name to lock.
@type name: string
'''
if iswindows:
mutexname = 'mutexforsingleinstanceof'+__appname__+name
mutex = win32event.CreateMutex(None, False, mutexname)
if mutex:
atexit.register(win32api.CloseHandle, mutex)
return not win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS
else:
path = os.path.expanduser('~/.'+__appname__+'_'+name+'.lock')
try:
f = open(path, 'w')
fcntl.lockf(f.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
atexit.register(_clean_lock_file, f)
return True
except IOError:
return False
return False

View File

@ -49,7 +49,7 @@ wherever possible in this module.
Get command line arguments as unicode objects. Note that the
first argument will be the path to the interpreter, *not* the
script being run. So to replace sys.argv, you should use
sys.argv[1:] = winutil.argv()[1:].
`if len(sys.argv) > 1: sys.argv[1:] = winutil.argv()[1-len(sys.argv):]`
*/

View File

@ -1028,11 +1028,13 @@ class ZipFile:
# Create all upper directories if necessary.
upperdirs = os.path.dirname(targetpath)
if upperdirs and os.path.exists(upperdirs) and not os.path.isdir(upperdirs):
os.unlink(upperdirs)
if upperdirs and not os.path.exists(upperdirs):
os.makedirs(upperdirs)
source = self.open(member, pwd=pwd)
target = file(targetpath, "wb")
target = open(targetpath, "wb")
shutil.copyfileobj(source, target)
source.close()
target.close()

View File

@ -12,9 +12,10 @@ from urllib import url2pathname
from httplib import responses
from calibre import setup_cli_handlers, browser, sanitize_file_name, \
OptionParser, relpath, LoggingInterface
relpath, LoggingInterface
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.chardet import xml_to_unicode
from calibre.utils.config import OptionParser
class FetchError(Exception):
pass

View File

@ -28,9 +28,8 @@ MOBILEREAD = 'ftp://dev.mobileread.com/calibre/'
BUILD_SCRIPT ='''\
#!/bin/bash
cd ~/build && \
rsync -avz --exclude src/calibre/plugins --exclude docs --exclude .bzr --exclude .build --exclude build --exclude dist --exclude "*.pyc" --exclude "*.pyo" rsync://%(host)s/work/%(project)s . && \
cd %(project)s && rm -rf build dist src/calibre/plugins && \
mkdir -p build dist src/calibre/plugins && \
rsync -avz --exclude src/calibre/plugins --exclude calibre/src/calibre.egg-info --exclude docs --exclude .bzr --exclude .build --exclude build --exclude dist --exclude "*.pyc" --exclude "*.pyo" rsync://%(host)s/work/%(project)s . && \
cd %(project)s && \
%%s && \
rm -rf build/* && \
%%s %%s
@ -76,6 +75,8 @@ def build_windows(shutdown=True):
installer = installer_name('exe')
vm = '/vmware/Windows XP/Windows XP Professional.vmx'
start_vm(vm, 'windows', BUILD_SCRIPT%('python setup.py develop', 'python','installer\\\\windows\\\\freeze.py'))
if os.path.exists('build/py2exe'):
shutil.rmtree('build/py2exe')
subprocess.check_call(('scp', '-rp', 'windows:build/%s/build/py2exe'%PROJECT, 'build'))
if not os.path.exists('build/py2exe'):
raise Exception('Failed to run py2exe')
@ -217,12 +218,12 @@ def upload_src_tarball():
check_call('scp dist/calibre-*.tar.bz2 divok:%s/'%DOWNLOADS)
def stage_one():
shutil.rmtree('build')
check_call('sudo rm -rf build', shell=True)
os.mkdir('build')
shutil.rmtree('docs')
os.mkdir('docs')
check_call(['python', 'setup.py', 'build'])
check_call('sudo rm -f src/%s/gui2/images_rc.pyc'%__appname__, shell=True)
check_call('python setup.py build', shell=True)
check_call('sudo python setup.py develop', shell=True)
check_call('make', shell=True)
tag_release()
upload_demo()
@ -241,6 +242,7 @@ def stage_three():
upload_user_manual()
check_call('python setup.py register bdist_egg --exclude-source-files upload')
check_call('''rm -rf dist/* build/*''')
check_call('''ssh divok bzr update /var/www/calibre.kovidgoyal.net/calibre/''')
def main(args=sys.argv):
print 'Starting stage one...'