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' HOME = '/home/kovid'
PYINSTALLER = os.path.expanduser('~/build/pyinstaller') PYINSTALLER = os.path.expanduser('~/build/pyinstaller')
CALIBREPREFIX = '___' CALIBREPREFIX = '___'
CLIT = '/usr/bin/clit'
PDFTOHTML = '/usr/bin/pdftohtml' PDFTOHTML = '/usr/bin/pdftohtml'
LIBUNRAR = '/usr/lib/libunrar.so' LIBUNRAR = '/usr/lib/libunrar.so'
QTDIR = '/usr/lib/qt4' QTDIR = '/usr/lib/qt4'
@ -20,7 +19,9 @@ SQLITE = '/usr/lib/libsqlite3.so.0'
DBUS = '/usr/lib/libdbus-1.so.3' DBUS = '/usr/lib/libdbus-1.so.3'
LIBMNG = '/usr/lib/libmng.so.1' LIBMNG = '/usr/lib/libmng.so.1'
LIBZ = '/lib/libz.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') 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')] binaries += [(os.path.basename(f), f, 'BINARY')]
print 'Adding external programs...' print 'Adding external programs...'
binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'), binaries += [('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')] ('libunrar.so', LIBUNRAR, 'BINARY')]
print 'Adding external libraries...' 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 = [] qt = []

View File

@ -243,12 +243,6 @@ _check_symlinks_prescript()
self.add_plugins() 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
print 'Adding pdftohtml' print 'Adding pdftohtml'
os.link(os.path.expanduser('~/pdftohtml'), os.path.join(frameworks_dir, '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 ::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 ::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 ::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 ::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 ::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 ::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 ::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 ::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 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 ::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 ::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 ::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 ::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 ::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 ::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 ::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 ::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 ::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 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 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' DEVCON = 'C:\\devcon\\i386\\devcon.exe'
LIBUSB_DIR = 'C:\\libusb' LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll' LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
CLIT = 'C:\\clit\\clit.exe'
PDFTOHTML = 'C:\\pdftohtml\\pdftohtml.exe' PDFTOHTML = 'C:\\pdftohtml\\pdftohtml.exe'
IMAGEMAGICK_DIR = 'C:\\ImageMagick' IMAGEMAGICK_DIR = 'C:\\ImageMagick'
FONTCONFIG_DIR = 'C:\\fontconfig' 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))) shutil.copyfile(DEVCON, os.path.join(tdir, os.path.basename(DEVCON)))
print '\tAdding unrar' print '\tAdding unrar'
shutil.copyfile(LIBUNRAR, os.path.join(PY2EXE_DIR, os.path.basename(LIBUNRAR))) 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' print '\tAdding pdftohtml'
shutil.copyfile(PDFTOHTML, os.path.join(PY2EXE_DIR, os.path.basename(PDFTOHTML))) shutil.copyfile(PDFTOHTML, os.path.join(PY2EXE_DIR, os.path.basename(PDFTOHTML)))
print '\tAdding ImageMagick' 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) iswindows = re.search('win(32|64)', sys.platform)
isosx = 'darwin' in sys.platform isosx = 'darwin' in sys.platform
islinux = not isosx and not iswindows 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) VERSION = re.search(r'__version__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1)
APPNAME = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1) APPNAME = re.search(r'__appname__\s+=\s+[\'"]([^\'"]+)[\'"]', src).group(1)
print 'Setup', APPNAME, 'version:', VERSION print 'Setup', APPNAME, 'version:', VERSION
@ -47,17 +47,25 @@ main_functions = {
if __name__ == '__main__': if __name__ == '__main__':
from setuptools import setup, find_packages, Extension from setuptools import setup, find_packages, Extension
from pyqtdistutils import PyQtExtension, build_ext
import subprocess, glob import subprocess, glob
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install') 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', sources=['src/calibre/utils/lzx/lzxmodule.c',
'src/calibre/utils/lzx/lzxd.c'], 'src/calibre/utils/lzx/lzxd.c'],
include_dirs=['src/calibre/utils/lzx']), include_dirs=['src/calibre/utils/lzx']),
Extension('calibre.plugins.msdes', Extension('calibre.plugins.msdes',
sources=['src/calibre/utils/msdes/msdesmodule.c', sources=['src/calibre/utils/msdes/msdesmodule.c',
'src/calibre/utils/msdes/des.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: if iswindows:
ext_modules.append(Extension('calibre.plugins.winutil', ext_modules.append(Extension('calibre.plugins.winutil',
sources=['src/calibre/utils/windows/winutil.c'], sources=['src/calibre/utils/windows/winutil.c'],
@ -69,42 +77,6 @@ if __name__ == '__main__':
sources=['src/calibre/devices/usbobserver/usbobserver.c']) 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( setup(
name=APPNAME, name=APPNAME,
packages = find_packages('src'), packages = find_packages('src'),
@ -152,7 +124,8 @@ if __name__ == '__main__':
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Hardware :: Hardware Drivers' 'Topic :: System :: Hardware :: Hardware Drivers'
] ],
cmdclass = {'build_ext': build_ext},
) )
if 'develop' in ' '.join(sys.argv) and islinux: if 'develop' in ' '.join(sys.argv) and islinux:

View File

@ -1,122 +1,28 @@
''' E-book management software''' ''' E-book management software'''
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__version__ = '0.4.80' __docformat__ = 'restructuredtext en'
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid at kovidgoyal.net>"
__appname__ = 'calibre'
import sys, os, logging, mechanize, locale, copy, cStringIO, re, subprocess, \ import sys, os, re, logging, time, subprocess, mechanize, atexit
textwrap, atexit, cPickle, codecs, time
from gettext import GNUTranslations
from htmlentitydefs import name2codepoint from htmlentitydefs import name2codepoint
from math import floor from math import floor
from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from logging import Formatter from logging import Formatter
from PyQt4.QtCore import QSettings, QVariant, QUrl, QByteArray, QString from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QDesktopServices from PyQt4.QtGui import QDesktopServices
from calibre.startup import plugins, winutil, winutilerror
from calibre.translations.msgfmt import make from calibre.constants import iswindows, isosx, islinux, isfrozen, \
from calibre.ebooks.chardet import detect terminal_controller, preferred_encoding, \
from calibre.utils.terminfo import TerminalController __appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl
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))
plugins = {} def unicode_path(path, abs=False):
for plugin in ['pictureflow', 'lzx', 'msdes'] + \ if not isinstance(path, unicode):
(['winutil'] if iswindows else []) + \ path = path.decode(sys.getfilesystemencoding())
(['usbobserver'] if isosx else []): if abs:
try: path = os.path.abspath(path)
p, err = __import__(plugin), '' return path
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 osx_version(): def osx_version():
if isosx: if isosx:
@ -127,10 +33,6 @@ def osx_version():
return int(m.group(1)), int(m.group(2)), int(m.group(3)) 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): class CommandLineError(Exception):
pass pass
@ -172,121 +74,6 @@ def setup_cli_handlers(logger, level):
logger.addHandler(handler) 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): def load_library(name, cdll):
if iswindows: if iswindows:
@ -319,36 +106,37 @@ def extract(path, dir):
extractor(path, dir) extractor(path, dir)
def get_proxies(): def get_proxies():
proxies = {} proxies = {}
if iswindows:
try: for q in ('http', 'ftp'):
winreg = __import__('_winreg') proxy = os.environ.get(q+'_proxy', None)
settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER, if not proxy: continue
'Software\\Microsoft\\Windows' if proxy.startswith(q+'://'):
'\\CurrentVersion\\Internet Settings') proxy = proxy[7:]
proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0] proxies[q] = proxy
if proxy:
server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0]) if iswindows:
if ';' in server: try:
for p in server.split(';'): winreg = __import__('_winreg')
protocol, address = p.split('=') settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
proxies[protocol] = address 'Software\\Microsoft\\Windows'
else: '\\CurrentVersion\\Internet Settings')
proxies['http'] = server proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
proxies['ftp'] = server if proxy:
settings.Close() server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
except Exception, e: if ';' in server:
print('Unable to detect proxy settings: %s' % str(e)) for p in server.split(';'):
if proxies: protocol, address = p.split('=')
print('Using proxies: %s' % proxies) proxies[protocol] = address
else: else:
for q in ('http', 'ftp'): proxies['http'] = server
proxy = os.environ.get(q+'_proxy', None) proxies['ftp'] = server
if not proxy: continue settings.Close()
if proxy.startswith(q+'://'): except Exception, e:
proxy = proxy[7:] print('Unable to detect proxy settings: %s' % str(e))
proxies[q] = proxy if proxies:
return proxies print('Using proxies: %s' % proxies)
return proxies
def browser(honor_time=False): def browser(honor_time=False):
@ -383,40 +171,6 @@ def fit_image(width, height, pwidth, pheight):
return scaled, int(width), int(height) 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): 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:] rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:]
return os.path.join(*rel_list) 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) _spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
def english_sort(x, y): def english_sort(x, y):
''' '''
@ -662,10 +302,7 @@ def strftime(fmt, t=time.localtime()):
A version of strtime that returns unicode strings. A version of strtime that returns unicode strings.
''' '''
result = time.strftime(fmt, t) result = time.strftime(fmt, t)
try: return unicode(result, preferred_encoding, 'replace')
return unicode(result, locale.getpreferredencoding(), 'replace')
except:
return unicode(result, 'utf-8', 'replace')
def entity_to_unicode(match, exceptions=[], encoding='cp1252'): 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')): if not os.path.exists(os.path.join(fdir, 'LiberationSans_Regular.ttf')):
from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts from calibre.ebooks.lrf.fonts.liberation import __all__ as fonts
for font in fonts: for font in fonts:
exec 'from calibre.ebooks.lrf.fonts.liberation.'+font+' import font_data' l = {}
open(os.path.join(fdir, font+'.ttf'), 'wb').write(font_data) 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 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 from calibre.libunzip import update
def option_parser(): def option_parser():
@ -25,7 +26,7 @@ Run an embedded python interpreter.
def update_zipfile(zipfile, mod, path): def update_zipfile(zipfile, mod, path):
if 'win32' in sys.platform: 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 '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() raw_input()
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*') pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1] name = mod.replace('.', '/') + os.path.splitext(path)[-1]
@ -35,8 +36,13 @@ def update_zipfile(zipfile, mod, path):
def update_module(mod, path): def update_module(mod, path):
if not hasattr(sys, 'frozen'): if not hasattr(sys, 'frozen'):
raise RuntimeError('Modules can only be updated in frozen installs.') 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') 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) update_zipfile(zp, mod, path)
else: else:
raise ValueError('Updating modules is not supported on this platform.') 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 import __version__, iswindows, __appname__
from calibre.devices.errors import PathError 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.errors import ArgumentError, DeviceError, DeviceLocked
from calibre.devices import devices from calibre.devices import devices
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner

View File

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

View File

@ -14,7 +14,8 @@ from calibre.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \
Paragraph, TextStyle, BlockStyle Paragraph, TextStyle, BlockStyle
from calibre.ebooks.lrf.fonts import FONT_FILE_MAP from calibre.ebooks.lrf.fonts import FONT_FILE_MAP
from calibre.ebooks import ConversionError 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" __docformat__ = "epytext"

View File

@ -121,7 +121,7 @@ class PageProcessor(list):
DestroyMagickWand(img) DestroyMagickWand(img)
MagickCropImage(split1, (width/2)-1, height, 0, 0) MagickCropImage(split1, (width/2)-1, height, 0, 0)
MagickCropImage(split2, (width/2)-1, height, width/2, 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() self.process_pages()
except Exception, err: except Exception, err:
@ -180,7 +180,7 @@ class PageProcessor(list):
MagickSetImageType(wand, GrayscaleType) MagickSetImageType(wand, GrayscaleType)
MagickQuantizeImage(wand, self.opts.colors, RGBColorspace, 0, 1, 0) 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) dest = os.path.join(self.dest, dest)
MagickWriteImage(wand, dest) MagickWriteImage(wand, dest)
self.append(dest) self.append(dest)
@ -219,7 +219,7 @@ def process_pages(pages, opts, update):
for pp in processed_pages: for pp in processed_pages:
if len(pp) == 0: if len(pp) == 0:
failures.append(os.path.basename(pp.path_to_page())) failures.append(os.path.basename(pp.path_to_page))
else: else:
ans += pp ans += pp
return ans, failures, tdir return ans, failures, tdir
@ -238,7 +238,7 @@ def config(defaults=None):
c.add_opt('output', ['-o', '--output'], c.add_opt('output', ['-o', '--output'],
help=_('Path to output LRF file. By default a file is created in the current directory.')) 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, 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, c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
help=_('Disable normalize (improve contrast) color range for pictures. 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, c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
@ -247,6 +247,8 @@ def config(defaults=None):
help=_('Disable sharpening.')) help=_('Disable sharpening.'))
c.add_opt('landscape', ['-l', '--landscape'], default=False, c.add_opt('landscape', ['-l', '--landscape'], default=False,
help=_("Don't split landscape images into two portrait images")) 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, 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.")) 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(), 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.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPF from calibre.ebooks.metadata.opf import OPF
from calibre.ebooks.metadata.epub import OCFDirReader 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 from calibre import __appname__, setup_cli_handlers
@ -26,7 +26,7 @@ def generate_html(pathtoepub, logger):
tdir = mkdtemp(prefix=__appname__+'_') tdir = mkdtemp(prefix=__appname__+'_')
os.rmdir(tdir) os.rmdir(tdir)
try: try:
zip_extract(pathtoepub, tdir) ZipFile(pathtoepub).extractall(tdir)
except: except:
if os.path.exists(tdir) and os.path.isdir(tdir): if os.path.exists(tdir) and os.path.isdir(tdir):
shutil.rmtree(tdir) shutil.rmtree(tdir)

View File

@ -2,7 +2,8 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, logging, os 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 import ConversionError
from calibre.ebooks.lrf.meta import get_metadata from calibre.ebooks.lrf.meta import get_metadata
from calibre.ebooks.lrf.lrfparser import LRFDocument 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, \ from PyQt4.Qt import QWebPage, QUrl, QApplication, QSize, \
SIGNAL, QPainter, QImage, QObject, Qt SIGNAL, QPainter, QImage, QObject, Qt
from calibre.parallel import ParallelJob
__app = None __app = None
class HTMLTableRenderer(QObject): class HTMLTableRenderer(QObject):
@ -80,18 +82,17 @@ def render_table(server, soup, table, css, base_dir, width, height, dpi, factor=
</body> </body>
</html> </html>
'''%(head, width-10, style, unicode(table)) '''%(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]) args=[html, base_dir, width, height, dpi, factor])
res = None server.add_job(job)
while res is None: while not job.has_run:
time.sleep(2) time.sleep(2)
res = server.result(1)
result, exception, traceback = res if job.exception is not None:
if exception:
print 'Failed to render table' print 'Failed to render table'
print exception print job.exception
print traceback print job.traceback
images, tdir = result images, tdir = job.result
atexit.register(shutil.rmtree, tdir) atexit.register(shutil.rmtree, tdir)
return images return images

View File

@ -3,19 +3,12 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, shutil, glob, logging import os, sys, shutil, glob, logging
from tempfile import mkdtemp from tempfile import mkdtemp
from subprocess import Popen, PIPE
from calibre.ebooks.lrf import option_parser as lrf_option_parser from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.lit.reader import LitReader from calibre.ebooks.lit.reader import LitReader
from calibre.ebooks import ConversionError from calibre.ebooks import ConversionError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPFReader from calibre.ebooks.metadata.opf import OPFReader
from calibre import isosx, __appname__, setup_cli_handlers, islinux from calibre import __appname__, setup_cli_handlers
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')
def option_parser(): def option_parser():
parser = lrf_option_parser( parser = lrf_option_parser(
@ -24,37 +17,15 @@ _('''Usage: %prog [options] mybook.lit
%prog converts mybook.lit to mybook.lrf''') %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 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): def generate_html(pathtolit, logger):
if not os.access(pathtolit, os.R_OK): if not os.access(pathtolit, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtolit raise ConversionError, 'Cannot read from ' + pathtolit
tdir = mkdtemp(prefix=__appname__+'_') tdir = mkdtemp(prefix=__appname__+'_'+'lit2oeb_')
os.rmdir(tdir) lr = LitReader(pathtolit)
cmd = [CLIT, pathtolit, '%s'%(tdir+os.sep)] print 'Extracting LIT file to', tdir
logger.debug(repr(cmd)) lr.extract_content(tdir)
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
return tdir return tdir
def process_file(path, options, logger=None): def process_file(path, options, logger=None):
@ -63,8 +34,7 @@ def process_file(path, options, logger=None):
logger = logging.getLogger('lit2lrf') logger = logging.getLogger('lit2lrf')
setup_cli_handlers(logger, level) setup_cli_handlers(logger, level)
lit = os.path.abspath(os.path.expanduser(path)) lit = os.path.abspath(os.path.expanduser(path))
tdir = generate_html2(lit, logger) if getattr(options, 'lit2oeb', False) \ tdir = generate_html(lit, logger)
else generate_html(lit, logger)
try: try:
opf = glob.glob(os.path.join(tdir, '*.opf')) opf = glob.glob(os.path.join(tdir, '*.opf'))
if 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.basename(os.path.splitext(path)[0]) + ext)
options.output = os.path.abspath(os.path.expanduser(options.output)) options.output = os.path.abspath(os.path.expanduser(options.output))
options.use_spine = True options.use_spine = True
html_process_file(htmlfile, options, logger=logger) html_process_file(htmlfile, options, logger=logger)
finally: finally:
try: try:

View File

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

View File

@ -6,7 +6,8 @@ Compile a LRS file into a LRF file.
import sys, os, logging 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, \ from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, \
CData, Tag CData, Tag
from calibre.ebooks.lrf.pylrs.pylrs import Book, PageStyle, TextStyle, \ from calibre.ebooks.lrf.pylrs.pylrs import Book, PageStyle, TextStyle, \

View File

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

View File

@ -25,6 +25,8 @@ def generate_html(pathtopdf, logger):
Convert the pdf into html. Convert the pdf into html.
@return: Path to a temporary file containing the 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): if not os.access(pathtopdf, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtopdf raise ConversionError, 'Cannot read from ' + pathtopdf
tdir = PersistentTemporaryDirectory('pdftohtml') 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 import sys, os, re, tempfile, subprocess, atexit, shutil, logging, xml.parsers.expat
from xml.etree.ElementTree import parse 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 from calibre.ebooks import ConversionError
PDFTOXML = 'pdftoxml.exe' PDFTOXML = 'pdftoxml.exe'

View File

@ -11,8 +11,9 @@ from urllib import unquote, quote
from urlparse import urlparse from urlparse import urlparse
from calibre import __version__ as VERSION, relpath from calibre.constants import __version__ as VERSION
from calibre import OptionParser from calibre import relpath
from calibre.utils.config import OptionParser
def get_parser(extension): def get_parser(extension):
''' Return an option parser with the basic metadata options already setup''' ''' 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 import sys, logging, re, socket
from urllib import urlopen, quote 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.metadata import MetaInformation
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup 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 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 from calibre.ebooks.BeautifulSoup import BeautifulSoup
browser = None browser = None

View File

@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, collections 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.rtf import get_metadata as rtf_metadata
from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata
from calibre.ebooks.lrf.meta import get_metadata as lrf_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': elif stream_type == 'rtf':
set_rtf_metadata(stream, mi) 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): def metadata_from_filename(name, pat=None):
name = os.path.splitext(name)[0] name = os.path.splitext(name)[0]
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
if pat is None: if pat is None:
pat = _filename_pat pat = re.compile(prefs.get('filename_pattern'))
match = pat.search(name) match = pat.search(name)
if match: if match:
try: try:
@ -134,12 +126,12 @@ def metadata_from_filename(name, pat=None):
try: try:
si = match.group('series_index') si = match.group('series_index')
mi.series_index = int(si) mi.series_index = int(si)
except IndexError, ValueError: except (IndexError, ValueError):
pass pass
try: try:
si = match.group('isbn') si = match.group('isbn')
mi.isbn = si mi.isbn = si
except IndexError, ValueError: except (IndexError, ValueError):
pass pass
if not mi.title: if not mi.title:
mi.title = name mi.title = name

View File

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

View File

@ -2,19 +2,49 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """ """ The GUI """
import sys, os, re, StringIO, traceback 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 QByteArray, QLocale, QUrl, QTranslator, QCoreApplication
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication QIcon, QTableView, QDialogButtonBox, QApplication
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' 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 import calibre.resources as resources
NONE = QVariant() #: Null value to return from the data function of item models 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 # Turn off DeprecationWarnings in windows GUI
if iswindows: if iswindows:
import warnings import warnings
@ -84,20 +114,33 @@ def human_readable(size):
size = size[:-2] size = size[:-2]
return size + " " + suffix 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): class TableView(QTableView):
def __init__(self, parent): def __init__(self, parent):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.read_settings() self.read_settings()
def read_settings(self): 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): def write_settings(self):
settings = Settings() dynamic[self.__class__.__name__+'column widths'] = \
settings.set(self.__class__.__name__ + ' column widths', tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))])
tuple([int(self.columnWidth(i)) for i in range(self.model().columnCount(None))]))
def restore_column_widths(self): def restore_column_widths(self):
if self.cw and len(self.cw): 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 @param cols: A list of booleans or None. If an entry is False the corresponding column
is hidden, if True it is shown. is hidden, if True it is shown.
''' '''
key = self.__class__.__name__+'visible columns'
if cols: if cols:
Settings().set(self.__class__.__name__ + ' visible columns', cols) dynamic[key] = cols
else: else:
cols = Settings().get(self.__class__.__name__ + ' visible columns') cols = dynamic[key]
if not cols: if not cols:
cols = [True for i in range(self.model().columnCount(self))] cols = [True for i in range(self.model().columnCount(self))]
@ -217,7 +261,7 @@ _sidebar_directories = []
def set_sidebar_directories(dirs): def set_sidebar_directories(dirs):
global _sidebar_directories global _sidebar_directories
if dirs is None: if dirs is None:
dirs = Settings().get('frequently used directories', []) dirs = config['frequently_used_directories']
_sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs] _sidebar_directories = [QUrl.fromLocalFile(i) for i in dirs]
class FileDialog(QObject): class FileDialog(QObject):
@ -240,10 +284,10 @@ class FileDialog(QObject):
if add_all_files_filter or not ftext: if add_all_files_filter or not ftext:
ftext += 'All files (*)' ftext += 'All files (*)'
settings = Settings()
self.dialog_name = name if name else 'dialog_' + title self.dialog_name = name if name else 'dialog_' + title
self.selected_files = None self.selected_files = None
self.fd = None self.fd = None
if islinux: if islinux:
self.fd = QFileDialog(parent) self.fd = QFileDialog(parent)
self.fd.setFileMode(mode) self.fd.setFileMode(mode)
@ -251,15 +295,15 @@ class FileDialog(QObject):
self.fd.setModal(modal) self.fd.setModal(modal)
self.fd.setFilter(ftext) self.fd.setFilter(ftext)
self.fd.setWindowTitle(title) self.fd.setWindowTitle(title)
state = settings.get(self.dialog_name, QByteArray()) state = dynamic[self.dialog_name]
if not self.fd.restoreState(state): if not state or not self.fd.restoreState(state):
self.fd.setDirectory(os.path.expanduser('~')) self.fd.setDirectory(os.path.expanduser('~'))
osu = [i for i in self.fd.sidebarUrls()] osu = [i for i in self.fd.sidebarUrls()]
self.fd.setSidebarUrls(osu + _sidebar_directories) self.fd.setSidebarUrls(osu + _sidebar_directories)
QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir) QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir)
self.accepted = self.fd.exec_() == QFileDialog.Accepted self.accepted = self.fd.exec_() == QFileDialog.Accepted
else: else:
dir = settings.get(self.dialog_name, os.path.expanduser('~')) dir = dynamic.get(self.dialog_name, default=os.path.expanduser('~'))
self.selected_files = [] self.selected_files = []
if mode == QFileDialog.AnyFile: if mode == QFileDialog.AnyFile:
f = qstring_to_unicode( f = qstring_to_unicode(
@ -284,7 +328,7 @@ class FileDialog(QObject):
self.selected_files.append(f) self.selected_files.append(f)
if self.selected_files: if self.selected_files:
self.selected_files = [qstring_to_unicode(q) for q in 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) self.accepted = bool(self.selected_files)
@ -298,8 +342,7 @@ class FileDialog(QObject):
def save_dir(self): def save_dir(self):
if self.fd: if self.fd:
settings = Settings() dynamic[self.dialog_name] = self.fd.saveState()
settings.set(self.dialog_name, self.fd.saveState())
def choose_dir(window, name, title): def choose_dir(window, name, title):

View File

@ -12,7 +12,8 @@ import sys, os
from PyQt4.QtGui import QImage, QSizePolicy from PyQt4.QtGui import QImage, QSizePolicy
from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject 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'] pictureflow, pictureflowerror = plugins['pictureflow']
if pictureflow is not None: if pictureflow is not None:
@ -70,7 +71,7 @@ if pictureflow is not None:
def __init__(self, height=300, parent=None): def __init__(self, height=300, parent=None):
pictureflow.PictureFlow.__init__(self, parent, 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.setSlideSize(QSize(int(2/3. * height), height))
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25)) self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+25))
self.setFocusPolicy(Qt.WheelFocus) self.setFocusPolicy(Qt.WheelFocus)

View File

@ -1,131 +1,185 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __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.devices import devices
from calibre.parallel import Job
from calibre.devices.scanner import DeviceScanner 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 Worker thread that polls the USB ports for devices. Emits the
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
disconnection events. 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 @param sleep_time: Time to sleep between device probes in millisecs
@type sleep_time: integer @type sleep_time: integer
''' '''
self.devices = [[d, False] for d in devices()] Thread.__init__(self)
self.sleep_time = sleep_time self.setDaemon(True)
QThread.__init__(self) self.devices = [[d, False] for d in devices()]
self.keep_going = True 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): def run(self):
scanner = DeviceScanner()
while self.keep_going: while self.keep_going:
scanner.scan() self.detect_device()
for device in self.devices: while True:
connected = scanner.is_device_connected(device[0]) job = self.next()
if connected and not device[1]: if job is not None:
try: self.current_job = job
dev = device[0]() self.device.set_progress_reporter(job.update_status)
dev.open() self.current_job.run()
self.emit(SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), dev, True) self.current_job = None
except: else:
print 'Unable to open device' break
traceback.print_exc() time.sleep(self.sleep_time)
finally:
device[1] = True def create_job(self, func, done, description, args=[], kwargs={}):
elif not connected and device[1]: job = DeviceJob(func, done, self.job_manager,
self.emit(SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), device[0], False) args=args, kwargs=kwargs, description=description)
device[1] ^= True self.job_manager.add_job(job)
self.msleep(self.sleep_time) 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 books(self, done):
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):
'''Return callable that returns the list of books on device as two booklists''' '''Return callable that returns the list of books on device as two booklists'''
def books(updater): return self.create_job(self._books, done, description=_('Get list of books on device'))
'''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
def sync_booklists_func(self): def _sync_booklists(self, booklists):
'''Upload booklists to device''' '''Sync metadata to device'''
def sync_booklists(updater, booklists): self.device.sync_booklists(booklists, end_session=False)
'''Sync metadata to device''' return self.device.card_prefix(end_session=False), self.device.free_space()
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 upload_books_func(self): def sync_booklists(self, done, booklists):
'''Upload books to device''' return self.create_job(self._sync_booklists, done, args=[booklists],
def upload_books(updater, files, names, on_card=False): description=_('Send metadata to device'))
'''Upload books to device: '''
self.device.set_progress_reporter(updater) def _upload_books(self, files, names, on_card=False):
return self.device.upload_books(files, names, on_card, end_session=False) '''Upload books to device: '''
return upload_books 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): 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''' '''Remove books from device'''
def delete_books(updater, paths): self.device.delete_books(paths, end_session=True)
'''Remove books from device'''
self.device.delete_books(paths, end_session=True) def delete_books(self, done, paths):
return delete_books return self.create_job(self._delete_books, done, args=[paths],
description=_('Delete books from device'))
def remove_books_from_metadata(self, paths, booklists): 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''' '''Copy books from device to disk'''
def save_books(updater, paths, target): for path in paths:
'''Copy books from device to disk''' name = path.rpartition('/')[2]
self.device.set_progress_reporter(updater) f = open(os.path.join(target, name), '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 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')
self.device.get_file(path, f) self.device.get_file(path, f)
f.close() 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_() d.exec_()
def get_bulk_conversion_options(window): def get_bulk_conversion_options(window):
c = config(None) d = ComicConf(window, config_defaults=config(None).as_string())
with open(c.config_file_path, 'rb') as f:
d = ComicConf(window, config_defaults=f.read())
if d.exec_() == QDialog.Accepted: if d.exec_() == QDialog.Accepted:
return d.config.parse() return d.config.parse()
def get_conversion_options(window, defaults, title, author): def get_conversion_options(window, defaults, title, author):
if defaults is None: if defaults is None:
c = config(None) defaults = config(None).as_string()
with open(c.config_file_path, 'rb') as f:
defaults = f.read()
defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author)) defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author))
d = ComicConf(window, config_defaults=defaults, generic=False) d = ComicConf(window, config_defaults=defaults, generic=False)
if d.exec_() == QDialog.Accepted: if d.exec_() == QDialog.Accepted:
@ -61,6 +57,7 @@ class ComicConf(QDialog, Ui_Dialog):
self.opt_dont_sharpen.setChecked(opts.dont_sharpen) self.opt_dont_sharpen.setChecked(opts.dont_sharpen)
self.opt_landscape.setChecked(opts.landscape) self.opt_landscape.setChecked(opts.landscape)
self.opt_no_sort.setChecked(opts.no_sort) self.opt_no_sort.setChecked(opts.no_sort)
self.opt_right2left.setChecked(opts.right2left)
for opt in self.config.option_set.preferences: for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False) g = getattr(self, 'opt_'+opt.name, False)

View File

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

View File

@ -5,14 +5,14 @@ import os
from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon
from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant 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.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.gui2.widgets import FilenamePattern
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
class ConfigDialog(QDialog, Ui_Dialog): class ConfigDialog(QDialog, Ui_Dialog):
def __init__(self, window, db, columns): 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.item2 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.db = db self.db = db
self.current_cols = columns self.current_cols = columns
settings = Settings() path = prefs['library_path']
path = settings.get('library path')
self.location.setText(path) self.location.setText(path)
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact)
dirs = settings.get('frequently used directories', []) dirs = config['frequently_used_directories']
rn = settings.get('use roman numerals for series number', True) rn = config['use_roman_numerals_for_series_number']
self.timeout.setValue(settings.get('network timeout', 5)) self.timeout.setValue(prefs['network_timeout'])
self.roman_numerals.setChecked(rn) 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.directory_list.addItems(dirs)
self.connect(self.add_button, SIGNAL('clicked(bool)'), self.add_dir) self.connect(self.add_button, SIGNAL('clicked(bool)'), self.add_dir)
self.connect(self.remove_button, SIGNAL('clicked(bool)'), self.remove_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.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern) 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.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: for ext in BOOK_EXTENSIONS:
self.single_format.addItem(ext.upper(), QVariant(ext)) 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.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
self.cover_browse.setValue(settings.get('cover flow queue length', 6)) self.cover_browse.setValue(config['cover_flow_queue_length'])
self.confirm_delete.setChecked(settings.get('confirm delete', False)) self.confirm_delete.setChecked(config['confirm_delete'])
def compact(self, toggled): def compact(self, toggled):
d = Vacuum(self, self.db) d = Vacuum(self, self.db)
@ -88,19 +87,18 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.directory_list.takeItem(idx) self.directory_list.takeItem(idx)
def accept(self): def accept(self):
settings = Settings() config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
settings.set('use roman numerals for series number', bool(self.roman_numerals.isChecked())) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
settings.set('new version notification', bool(self.new_version_notification.isChecked())) prefs['network_timeout'] = int(self.timeout.value())
settings.set('network timeout', int(self.timeout.value()))
path = qstring_to_unicode(self.location.text()) path = qstring_to_unicode(self.location.text())
self.final_columns = [self.columns.item(i).checkState() == Qt.Checked for i in range(self.columns.count())] 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()]) config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
settings.set('show text in toolbar', bool(self.show_toolbar_text.isChecked())) config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
settings.set('confirm delete', bool(self.confirm_delete.isChecked())) config['confirm_delete'] = bool(self.confirm_delete.isChecked())
pattern = self.filename_pattern.commit() pattern = self.filename_pattern.commit()
settings.set('filename pattern', pattern) prefs['filename_pattern'] = pattern
settings.set('save to disk single format', BOOK_EXTENSIONS[self.single_format.currentIndex()]) config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()]
settings.set('cover flow queue length', self.cover_browse.value()) config['cover_flow_queue_length'] = self.cover_browse.value()
if not path or not os.path.exists(path) or not os.path.isdir(path): if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'), d = error_dialog(self, _('Invalid database location'),
@ -113,7 +111,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
else: else:
self.database_location = os.path.abspath(path) 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())] 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) QDialog.accept(self)
class Vacuum(QMessageBox): 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.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog from calibre.gui2 import error_dialog, NONE, info_dialog
from calibre.ebooks.metadata.isbndb import create_books, option_parser, ISBNDBError from calibre.ebooks.metadata.isbndb import create_books, option_parser, ISBNDBError
from calibre import Settings from calibre.utils.config import prefs
class Matches(QAbstractTableModel): class Matches(QAbstractTableModel):
@ -76,7 +76,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.timeout = timeout self.timeout = timeout
QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata) 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.setWindowTitle(title if title else 'Unknown')
self.tlabel.setText(self.tlabel.text().arg(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')) _('You must specify a valid access key for isbndb.com'))
return return
else: else:
Settings().set('isbndb.com key', key) prefs['isbndb_com_key'] = key
args = ['isbndb'] args = ['isbndb']
if self.isbn: if self.isbn:

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Display active jobs''' '''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, \ from PyQt4.QtGui import QDialog, QAbstractItemDelegate, QStyleOptionProgressBarV2, \
QApplication, QStyle QApplication, QStyle
@ -45,10 +45,20 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.pb_delegate = ProgressBarDelegate(self) self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) 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): def kill_job(self):
for index in self.jobs_view.selectedIndexes(): for index in self.jobs_view.selectedIndexes():
row = index.row() row = index.row()
self.emit(SIGNAL('kill_job(int, PyQt_PyObject)'), row, self) self.model.kill_job(row, self)
return return
def closeEvent(self, e): 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.lrf_single_ui import Ui_LRFSingleDialog
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2 import qstring_to_unicode, error_dialog, \ 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.gui2.widgets import FontFamilyModel
from calibre.ebooks.lrf import option_parser from calibre.ebooks.lrf import option_parser
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import __appname__, Settings from calibre.constants import __appname__
font_family_model = None font_family_model = None
@ -109,7 +109,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
def load_saved_global_defaults(self): def load_saved_global_defaults(self):
cmdline = Settings().get('LRF conversion defaults') cmdline = config['LRF_conversion_defaults']
if cmdline: if cmdline:
self.set_options_from_cmdline(cmdline) self.set_options_from_cmdline(cmdline)
@ -163,7 +163,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
def select_cover(self, checked): def select_cover(self, checked):
files = choose_images(self, 'change cover dialog', 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: if not files:
return return
_file = files[0] _file = files[0]
@ -385,7 +385,7 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
cmdline.extend([u'--cover', self.cover_file.name]) cmdline.extend([u'--cover', self.cover_file.name])
self.cmdline = [unicode(i) for i in cmdline] self.cmdline = [unicode(i) for i in cmdline]
else: else:
Settings().set('LRF conversion defaults', cmdline) config.set('LRF_conversion_defaults', cmdline)
QDialog.accept(self) QDialog.accept(self)
class LRFBulkDialog(LRFSingleDialog): 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.gui2.dialogs.password import PasswordDialog
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError 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): class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None): 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()'), QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
self.remove_unused_series) self.remove_unused_series)
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author) 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)) self.title.setText(db.title(row))
isbn = db.isbn(self.id, index_is_id=True) isbn = db.isbn(self.id, index_is_id=True)
if not isbn: if not isbn:

View File

@ -1,12 +1,11 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from PyQt4.QtGui import QDialog, QLineEdit 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.dialogs.password_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode from calibre.gui2 import qstring_to_unicode, dynamic
from calibre import Settings
class PasswordDialog(QDialog, Ui_Dialog): class PasswordDialog(QDialog, Ui_Dialog):
@ -14,10 +13,12 @@ class PasswordDialog(QDialog, Ui_Dialog):
QDialog.__init__(self, window) QDialog.__init__(self, window)
Ui_Dialog.__init__(self) Ui_Dialog.__init__(self)
self.setupUi(self) self.setupUi(self)
self.cfg_key = re.sub(r'[^0-9a-zA-Z]', '_', name)
settings = Settings() un = dynamic[self.cfg_key+'__un']
un = settings.get(name+': un', u'') pw = dynamic[self.cfg_key+'__un']
pw = settings.get(name+': pw', u'') if not un: un = ''
if not pw: pw = ''
self.gui_username.setText(un) self.gui_username.setText(un)
self.gui_password.setText(pw) self.gui_password.setText(pw)
self.sname = name self.sname = name
@ -37,7 +38,6 @@ class PasswordDialog(QDialog, Ui_Dialog):
return qstring_to_unicode(self.gui_password.text()) return qstring_to_unicode(self.gui_password.text())
def accept(self): def accept(self):
settings = Settings() dynamic.set(self.cfg_key+'__un', unicode(self.gui_username.text()))
settings.set(self.sname+': un', unicode(self.gui_username.text())) dynamic.set(self.cfg_key+'__pw', unicode(self.gui_password.text()))
settings.set(self.sname+': pw', unicode(self.gui_password.text()))
QDialog.accept(self) 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, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
from calibre import Settings, preferred_encoding from calibre import preferred_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.database import LibraryDatabase, text_to_tokens 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): class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
@ -113,7 +113,7 @@ class BooksModel(QAbstractTableModel):
self.cover_cache.clear_cache() self.cover_cache.clear_cache()
def read_config(self): 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): 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.QtGui import QKeySequence, QPainter, QDialog, QSpinBox, QSlider, QIcon
from PyQt4.QtCore import Qt, QObject, SIGNAL, QCoreApplication, QThread 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.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.dialogs.conversion_error import ConversionErrorDialog
from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow from calibre.gui2.lrf_renderer.main_ui import Ui_MainWindow
from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig from calibre.gui2.lrf_renderer.config_ui import Ui_ViewerConfig
@ -102,13 +103,15 @@ class Main(MainWindow, Ui_MainWindow):
def configure(self, triggered): 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 = Config(self, opts)
d.exec_() d.exec_()
if d.result() == QDialog.Accepted: if d.result() == QDialog.Accepted:
opts.white_background = bool(d.white_background.isChecked()) opts.white_background = bool(d.white_background.isChecked())
opts.hyphenate = bool(d.hyphenate.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): def set_ebook(self, stream):
self.progress_bar.setMinimum(0) self.progress_bar.setMinimum(0)
@ -281,7 +284,9 @@ Read the LRF ebook book.lrf
return parser return parser
def normalize_settings(parser, opts): 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: for opt in parser.option_list:
if not opt.dest: if not opt.dest:
continue continue

View File

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

View File

@ -3,9 +3,9 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import StringIO, traceback, sys 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.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre import OptionParser from calibre.utils.config import OptionParser
def option_parser(usage='''\ def option_parser(usage='''\
Usage: %prog [options] Usage: %prog [options]
@ -36,11 +36,17 @@ class MainWindow(QMainWindow):
def __init__(self, opts, parent=None): def __init__(self, opts, parent=None):
QMainWindow.__init__(self, parent) 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): if getattr(opts, 'redirect', False):
self.__console_redirect = DebugWindow(self) self.__console_redirect = DebugWindow(self)
sys.stdout = sys.stderr = self.__console_redirect sys.stdout = sys.stderr = self.__console_redirect
self.__console_redirect.show() self.__console_redirect.show()
def unix_signal(self, signal):
print 'Received signal:', repr(signal)
def unhandled_exception(self, type, value, tb): def unhandled_exception(self, type, value, tb):
try: try:
sio = StringIO.StringIO() 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('import images_rc', 'from calibre.gui2 import images_rc')
dat = dat.replace('from library import', 'from calibre.gui2.library import') dat = dat.replace('from library import', 'from calibre.gui2.library import')
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets 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.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) 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 { class FlowImages : QObject {
%TypeHeaderCode %TypeHeaderCode
#include "../../pictureflow.h" #include <pictureflow.h>
%End %End
public: public:
@ -22,7 +22,7 @@ public:
class PictureFlow : QWidget { class PictureFlow : QWidget {
%TypeHeaderCode %TypeHeaderCode
#include "../../pictureflow.h" #include <pictureflow.h>
%End %End

View File

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

View File

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

View File

@ -10,7 +10,8 @@ Command line interface to the calibre database.
import sys, os import sys, os
from textwrap import TextWrapper 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: try:
from calibre.utils.single_qt_application import send_message from calibre.utils.single_qt_application import send_message
except: except:
@ -26,6 +27,7 @@ def get_parser(usage):
parser = OptionParser(usage) parser = OptionParser(usage)
go = parser.add_option_group('GLOBAL OPTIONS') 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.')) 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 return parser
def get_db(dbpath, options): 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)) widths = list(map(lambda x : 0, fields))
for i in db.data: for i in db.data:
for j, field in enumerate(fields): 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 screen_width = terminal_controller.COLS
if not screen_width: if not screen_width:
@ -97,8 +99,7 @@ List the books available in the calibre database.
parser.add_option('-s', '--search', default=None, 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.')) 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) 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): if not set(fields).issubset(FIELDS):
parser.print_help() parser.print_help()
print print
@ -424,7 +425,7 @@ For help on an individual command: %%prog command --help
return 1 return 1
command = eval('command_'+args[1]) command = eval('command_'+args[1])
dbpath = Settings().get('library path', os.path.expanduser('~')) dbpath = prefs['library_path']
return command(args[2:], dbpath) return command(args[2:], dbpath)

View File

@ -1413,7 +1413,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
mi.render(f) mi.render(f)
f.close() 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) data = self.format(idx, fmt, index_is_id=index_is_id)
fname = name +'.'+fmt.lower() fname = name +'.'+fmt.lower()
fname = sanitize_file_name(fname) 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.write('[see also]\nhttp://%s.kovidgoyal.net\n'%__appname__)
f.close() f.close()
manifest = [] manifest = []
os.environ['PATH'] += ':'+os.path.expanduser('~/bin')
for src in entry_points['console_scripts']: for src in entry_points['console_scripts']:
prog = src[:src.index('=')].strip() prog = src[:src.index('=')].strip()
if prog in ('prs500', 'pdf-meta', 'epub-meta', 'lit-meta', if prog in ('prs500', 'pdf-meta', 'epub-meta', 'lit-meta',
'markdown-calibre', 'calibre-debug', 'fb2-meta', 'markdown-calibre', 'calibre-debug', 'fb2-meta',
'calibre-fontconfig', 'calibre-parallel'): 'calibre-fontconfig', 'calibre-parallel'):
continue continue
help2man = ('help2man', prog, '--name', 'part of %s'%__appname__, help2man = ('help2man', prog, '--name', 'part of %s'%__appname__,
'--section', '1', '--no-info', '--include', '--section', '1', '--no-info', '--include',
f.name, '--manual', __appname__) f.name, '--manual', __appname__)

View File

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

View File

@ -17,7 +17,7 @@ E-book Format Conversion
What formats does |app| support conversion to/from? 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? 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()" 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? 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, \ import sys, os, gc, cPickle, traceback, atexit, cStringIO, time, signal, \
subprocess, socket, collections, binascii, re, thread, tempfile subprocess, socket, collections, binascii, re, thread, tempfile
from select import select from select import select
from functools import partial
from threading import RLock, Thread, Event from threading import RLock, Thread, Event
from calibre.ptempfile import PersistentTemporaryFile 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 += 'import sys; sys.frameworks_dir = "%s"; sys.frozen = "macosx_app"; '%fd
self.prefix += 'sys.path.insert(0, %s); '%repr(sp) self.prefix += 'sys.path.insert(0, %s); '%repr(sp)
if fd not in os.environ['PATH']: if fd not in os.environ['PATH']:
self.env['PATH'] = os.environ['PATH']+':'+fd self.env['PATH'] = os.environ['PATH']+':'+fd
self.env['PYTHONHOME'] = resources 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: else:
self.executable = os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel') \ self.executable = os.path.join(getattr(sys, 'frozen_path'), 'calibre-parallel') \
if isfrozen else 'calibre-parallel' if isfrozen else 'calibre-parallel'
@ -332,7 +333,7 @@ class Overseer(object):
def __init__(self, server, port, timeout=5): def __init__(self, server, port, timeout=5):
self.worker_status = mother.spawn_worker('127.0.0.1:'+str(port)) self.worker_status = mother.spawn_worker('127.0.0.1:'+str(port))
self.socket = server.accept()[0] 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.os = os
self.signal = signal self.signal = signal
self.on_probation = False self.on_probation = False
@ -341,7 +342,6 @@ class Overseer(object):
self.working = False self.working = False
self.timeout = timeout self.timeout = timeout
self.last_job_time = time.time() self.last_job_time = time.time()
self.job_id = None
self._stop = False self._stop = False
if not select([self.socket], [], [], 120)[0]: if not select([self.socket], [], [], 120)[0]:
raise RuntimeError(_('Could not launch worker process.')) raise RuntimeError(_('Could not launch worker process.'))
@ -406,16 +406,14 @@ class Overseer(object):
`job`: An instance of :class:`Job`. `job`: An instance of :class:`Job`.
''' '''
self.job_id = job.job_id
self.working = True 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() msg = self.read()
if msg != 'OK': if msg != 'OK':
raise ControlError('Failed to initialize job on worker %d:%s'%(self.worker_pid, msg)) 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.job = job
self.last_report = time.time() self.last_report = time.time()
job.start_work()
def control(self): def control(self):
''' '''
@ -433,7 +431,9 @@ class Overseer(object):
else: else:
if self.on_probation: if self.on_probation:
self.terminate() self.terminate()
return Result(None, ControlError('Worker process died unexpectedly'), '') self.job.result = None
self.job.exception = ControlError('Worker process died unexpectedly')
return
else: else:
self.on_probation = True self.on_probation = True
return return
@ -443,13 +443,14 @@ class Overseer(object):
return return
elif word == 'RESULT': elif word == 'RESULT':
self.write('OK') self.write('OK')
return Result(cPickle.loads(msg), None, None) self.job.result = cPickle.loads(msg)
return True
elif word == 'OUTPUT': elif word == 'OUTPUT':
self.write('OK') self.write('OK')
try: try:
self.output(''.join(cPickle.loads(msg))) self.job.output(''.join(cPickle.loads(msg)))
except: except:
self.output('Bad output message: '+ repr(msg)) self.job.output('Bad output message: '+ repr(msg))
elif word == 'PROGRESS': elif word == 'PROGRESS':
self.write('OK') self.write('OK')
percent = None percent = None
@ -457,45 +458,154 @@ class Overseer(object):
percent, msg = cPickle.loads(msg)[-1] percent, msg = cPickle.loads(msg)[-1]
except: except:
print 'Bad progress update:', repr(msg) print 'Bad progress update:', repr(msg)
if self.progress and percent is not None: if percent is not None:
self.progress(percent, msg) self.job.update_status(percent, msg)
elif word == 'ERROR': elif word == 'ERROR':
self.write('OK') self.write('OK')
return Result(None, *cPickle.loads(msg)) self.job.excetion, self.job.traceback = cPickle.loads(msg)
return True
else: else:
self.terminate() 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: if not self.worker_status.is_alive() or time.time() - self.last_report > 180:
self.terminate() 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): class Job(object):
def __init__(self, job_id, func, args, kwdargs, output, progress, done): def __init__(self, job_done, job_manager=None,
self.job_id = job_id 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.func = func
self.args = args self.done = self.job_done
self.kwdargs = kwdargs
self.output = output
self.progress = progress
self.done = 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): def remove_ipc_socket(path):
os = __import__('os') os = __import__('os')
@ -525,7 +635,7 @@ class Server(Thread):
atexit.register(remove_ipc_socket, self.port) atexit.register(remove_ipc_socket, self.port)
self.server_socket.listen(5) self.server_socket.listen(5)
self.number_of_workers = number_of_workers 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.killall)
atexit.register(self.close) atexit.register(self.close)
self.job_lock = RLock() self.job_lock = RLock()
@ -544,15 +654,8 @@ class Server(Thread):
def add_job(self, job): def add_job(self, job):
with self.job_lock: with self.job_lock:
self.jobs.append(job) self.jobs.append(job)
if job.job_manager is not None:
def store_result(self, result, id=None): job.job_manager.add_job(job)
if id:
with self.job_lock:
self.results[id] = result
def result(self, id):
with self.result_lock:
return self.results.pop(id, None)
def run(self): def run(self):
while True: while True:
@ -575,8 +678,9 @@ class Server(Thread):
o.initialize_job(job) o.initialize_job(job)
except Exception, err: except Exception, err:
o.terminate() o.terminate()
res = Result(None, unicode(err), traceback.format_exc()) job.exception = err
job.done(res) job.traceback = traceback.format_exc()
job.done()
o = None o = None
if o and o.is_viable(): if o and o.is_viable():
with self.working_lock: with self.working_lock:
@ -586,12 +690,14 @@ class Server(Thread):
done = [] done = []
for o in self.working: for o in self.working:
try: 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: except Exception, err:
res = Result(None, unicode(err), traceback.format_exc()) o.job.exception = err
o.job.traceback = traceback.format_exc()
o.terminate() o.terminate()
if isinstance(res, Result): o.job.done()
o.job.done(res)
done.append(o) done.append(o)
for o in done: for o in done:
self.working.remove(o) self.working.remove(o)
@ -611,32 +717,23 @@ class Server(Thread):
self.pool = [] self.pool = []
def kill(self, job_id): def kill(self, job):
with self.working_lock: with self.working_lock:
pop = None pop = None
for o in self.working: for o in self.working:
if o.job_id == job_id: if o.job == job or o == job:
o.terminate() try:
o.job.done(Result(self.KILL_RESULT, None, '')) o.terminate()
except: pass
o.job.exception = JobKilled(_('Job stopped by user'))
try:
o.job.done()
except: pass
pop = o pop = o
break break
if pop is not None: if pop is not None:
self.working.remove(pop) 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={}): def run_free_job(self, func, args=[], kwdargs={}):
pt = PersistentTemporaryFile('.pickle', '_IPC_') pt = PersistentTemporaryFile('.pickle', '_IPC_')
pt.write(cPickle.dumps((func, args, kwdargs))) 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'), ('ImageMagick', '6.3.5', 'imagemagick', 'imagemagick', 'ImageMagick'),
('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'), ('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'),
('dbus-python', '0.82.2', 'dbus-python', 'python-dbus', 'dbus-python'), ('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'), ('lxml', '1.3.3', 'lxml', 'python-lxml', 'python-lxml'),
('help2man', '1.36.4', 'help2man', 'help2man', 'help2man'), ('help2man', '1.36.4', 'help2man', 'help2man', 'help2man'),
] ]
@ -49,10 +48,7 @@ class Distribution(object):
'fedora':'Fedora 8', 'debian':'Debian Sid', 'generic': 'Generic Unix'} 'fedora':'Fedora 8', 'debian':'Debian Sid', 'generic': 'Generic Unix'}
MANUAL_MAP = { 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.4.0 and PyQt to at least 4.4.2</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>',
} }
def __init__(self, os): 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 = '' EMPTYSTRING = ''
from calibre import __appname__ from calibre.constants import __appname__
from calibre import __version__ as version from calibre.constants import __version__ as version
# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's # The normal pot-file header. msgmerge and Emacs's po-mode work better if it's
# there. # there.

View File

@ -6,36 +6,54 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: calibre 0.4.55\n" "Project-Id-Version: calibre 0.4.55\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-07-21 22:18+0000\n" "POT-Creation-Date: 2008-08-03 09:01+0000\n"
"PO-Revision-Date: 2008-07-22 05:27+0000\n" "PO-Revision-Date: 2008-07-22 05:50+0000\n"
"Last-Translator: Kovid Goyal <Unknown>\n" "Last-Translator: Kovid Goyal <Unknown>\n"
"Language-Team: ru\n" "Language-Team: ru\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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" "X-Generator: Launchpad (build Unknown)\n"
"Generated-By: pygettext.py 1.5\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 #, fuzzy
msgid "%sUsage%s: %s\n" msgid "%sUsage%s: %s\n"
msgstr "%sИспользовано%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 " msgid "Created by "
msgstr "Сделано " msgstr "Сделано "
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:112 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:113
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:146 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:147
#: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:174 #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:175
msgid "Unable to detect the %s disk drive. Try rebooting." msgid "Unable to detect the %s disk drive. Try rebooting."
msgstr "Не удалось определить диск %s. Попробуйте перезагрузиться." 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." msgid "The reader has no storage card connected."
msgstr "К ридеру не подключена карта памяти." 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 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:71
msgid "Set the title. Default: filename." msgid "Set the title. Default: filename."
msgstr "Укажите заголовок. По умолчанию: имя файла." msgstr "Укажите заголовок. По умолчанию: имя файла."
@ -50,14 +68,17 @@ msgstr ""
"умолчанию: %default" "умолчанию: %default"
#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/__init__.py:74 #: /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/__init__.py:174
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/opf.py:314 #: /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/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:278
#: /home/kovid/work/calibre/src/calibre/gui2/library.py:685 #: /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/gui2/main.py:926
#: /home/kovid/work/calibre/src/calibre/library/database.py:1387 #: /home/kovid/work/calibre/src/calibre/library/database.py:904
#: /home/kovid/work/calibre/src/calibre/library/database.py:1517 #: /home/kovid/work/calibre/src/calibre/library/database.py:1412
#: /home/kovid/work/calibre/src/calibre/library/database.py:1542
msgid "Unknown" msgid "Unknown"
msgstr "Неизвестно" msgstr "Неизвестно"
@ -414,6 +435,88 @@ msgstr ""
msgid "No file to convert specified." msgid "No file to convert specified."
msgstr "Не указан файл для преобразования." 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 #: /home/kovid/work/calibre/src/calibre/ebooks/lrf/epub/convert_from.py:17
msgid "" msgid ""
"Usage: %prog [options] mybook.epub\n" "Usage: %prog [options] mybook.epub\n"
@ -515,40 +618,40 @@ msgid ""
"%s" "%s"
msgstr "" 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 "" msgid ""
"An error occurred while processing a table: %s. Ignoring table markup." "An error occurred while processing a table: %s. Ignoring table markup."
msgstr "" 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 "" msgid ""
"Bad table:\n" "Bad table:\n"
"%s" "%s"
msgstr "" 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" msgid "Table has cell that is too large"
msgstr "" 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 "" msgid ""
"You have to save the website %s as an html file first and then run html2lrf " "You have to save the website %s as an html file first and then run html2lrf "
"on it." "on it."
msgstr "" 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" msgid "Could not read cover image: %s"
msgstr "" 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" msgid "Cannot read from: %s"
msgstr "" 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" msgid "Failed to process opf file"
msgstr "" 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 "" msgid ""
"Usage: %prog [options] mybook.html\n" "Usage: %prog [options] mybook.html\n"
"\n" "\n"
@ -559,7 +662,7 @@ msgid ""
"convert a whole tree of HTML files." "convert a whole tree of HTML files."
msgstr "" 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 "" msgid ""
"Usage: %prog [options] mybook.lit\n" "Usage: %prog [options] mybook.lit\n"
"\n" "\n"
@ -741,11 +844,10 @@ msgstr ""
msgid "Set the comment" msgid "Set the comment"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:100 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:117
msgid "mybook.epub" msgid "A comma separated list of tags to set"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/epub.py:100
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/fb2.py:50 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/fb2.py:50
msgid "Usage:" msgid "Usage:"
msgstr "" msgstr ""
@ -803,11 +905,11 @@ msgid ""
"Fetch a cover image for the book identified by ISBN from LibraryThing.com\n" "Fetch a cover image for the book identified by ISBN from LibraryThing.com\n"
msgstr "" 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" msgid "Usage: %s file.lit"
msgstr "" 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" msgid "Cover saved to"
msgstr "" msgstr ""
@ -819,22 +921,14 @@ msgstr ""
msgid "No filename specified." msgid "No filename specified."
msgstr "" 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" msgid "%prog [options] myebook.mobi"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:393 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:423
msgid "Output directory. Defaults to current directory."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader.py:412
msgid "Raw MOBI HTML saved in" msgid "Raw MOBI HTML saved in"
msgstr "" 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:22
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:26 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:26
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:36 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/fetch_metadata.py:36
@ -855,6 +949,7 @@ msgid "Comments"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info_ui.py:55 #: /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" msgid "Dialog"
msgstr "" msgstr ""
@ -869,6 +964,51 @@ msgstr ""
msgid "Choose Format" msgid "Choose Format"
msgstr "" 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 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:23
msgid "Basic" msgid "Basic"
msgstr "" msgstr ""
@ -877,47 +1017,47 @@ msgstr ""
msgid "Advanced" msgid "Advanced"
msgstr "" 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." msgid "<br>Must be a directory."
msgstr "" 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 " msgid "Invalid database location "
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:105 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config.py:107
#: /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" msgid "Invalid database location"
msgstr "" 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 " msgid "Invalid database location.<br>Cannot write to "
msgstr "" 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." msgid "Compacting database. This may take a while."
msgstr "" 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..." msgid "Compacting..."
msgstr "" 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 #: /home/kovid/work/calibre/src/calibre/gui2/main_ui.py:265
msgid "Configuration" msgid "Configuration"
msgstr "" 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)" msgid "&Location of books database (library1.db)"
msgstr "" 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" msgid "Browse for the new database location"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:219 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:222
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:237 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:241
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/config_ui.py:239 #: /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/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:266
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:285 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:285
@ -937,89 +1077,93 @@ msgstr ""
msgid "..." msgid "..."
msgstr "" 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" msgid "Use &Roman numerals for series number"
msgstr "" 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):" msgid "&Number of covers to show in browse mode (after restart):"
msgstr "" 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" msgid "Show notification when &new version is available"
msgstr "" 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:" msgid "Format for &single file save:"
msgstr "" 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:" msgid "&Priority for conversion jobs:"
msgstr "" 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:" msgid "Default network &timeout:"
msgstr "" 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 "" msgid ""
"Set the default timeout for network fetches (i.e. anytime we go out to the " "Set the default timeout for network fetches (i.e. anytime we go out to the "
"internet to get information)" "internet to get information)"
msgstr "" 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" msgid " seconds"
msgstr "" 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" msgid "Toolbar"
msgstr "" 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" msgid "Large"
msgstr "" 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" msgid "Medium"
msgstr "" 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" msgid "Small"
msgstr "" 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" msgid "&Button size in toolbar"
msgstr "" 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" msgid "Show &text in toolbar buttons"
msgstr "" 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" msgid "Select visible &columns in library view"
msgstr "" 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" msgid "Frequently used directories"
msgstr "" 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" msgid "Add a directory to the frequently used directories list"
msgstr "" 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" msgid "Remove a directory from the frequently used directories list"
msgstr "" 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" msgid "Free unused diskspace from the database"
msgstr "" 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" msgid "&Compact database"
msgstr "" 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" msgid "&Metadata from file name"
msgstr "" msgstr ""
@ -1151,7 +1295,6 @@ msgid "Convert %s to LRF"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single.py:108 #: /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" msgid "Set conversion defaults"
msgstr "" msgstr ""
@ -1415,10 +1558,6 @@ msgstr ""
msgid "Override<br>CSS" msgid "Override<br>CSS"
msgstr "" 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 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/lrf_single_ui.py:553
msgid "&Left Margin:" msgid "&Left Margin:"
msgstr "" msgstr ""
@ -2261,23 +2400,31 @@ msgstr ""
msgid "Bulk convert" msgid "Bulk convert"
msgstr "" 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." msgid " detected."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:309 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:311
msgid "Device: " msgid "Device: "
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:334 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:336
msgid "Connected " msgid "Connected "
msgstr "" 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" msgid "Device database corrupted"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:347 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:349
msgid "" msgid ""
"\n" "\n"
" <p>The database of books on the reader is corrupted. Try the " " <p>The database of books on the reader is corrupted. Try the "
@ -2293,190 +2440,222 @@ msgid ""
" " " "
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:399 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:401
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:473 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:475
msgid "" msgid ""
"<p>Books with the same title as the following already exist in the database. " "<p>Books with the same title as the following already exist in the database. "
"Add them anyway?<ul>" "Add them anyway?<ul>"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:402 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:404
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:476 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:478
msgid "Duplicates found!" msgid "Duplicates found!"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:435 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:437
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:448 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:450
msgid "Uploading books to device." msgid "Uploading books to device."
msgstr "" 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" msgid "No space on device"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:508 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:510
msgid "" msgid ""
"<p>Cannot upload books to device there is no more free space available " "<p>Cannot upload books to device there is no more free space available "
msgstr "" 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." msgid "Deleting books from device."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:578 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:586
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:599 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:607
msgid "Cannot edit metadata" msgid "Cannot edit metadata"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:578 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:586
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:599 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:607
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:694 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:702
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:764 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:772
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:834
msgid "No books selected" msgid "No books selected"
msgstr "" 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." msgid "Sending books to device."
msgstr "" 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" msgid "No suitable formats"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:678 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:686
msgid "" msgid ""
"Could not upload the following books to the device, as no suitable formats " "Could not upload the following books to the device, as no suitable formats "
"were found:<br><ul>%s</ul>" "were found:<br><ul>%s</ul>"
msgstr "" 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" msgid "Cannot save to disk"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:705 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:713
msgid "" msgid ""
"<p>Could not save the following books to disk, because the %s format is not " "<p>Could not save the following books to disk, because the %s format is not "
"available for them:<ul>" "available for them:<ul>"
msgstr "" 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" msgid "Could not save some ebooks"
msgstr "" 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 " msgid "Fetch news from "
msgstr "" 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 " msgid "Fetching news from "
msgstr "" 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." msgid "News fetched. Uploading to device."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:764 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:772
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:834
msgid "Cannot convert" msgid "Cannot convert"
msgstr "" 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" msgid "Starting Bulk conversion of %d books"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:905 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:832
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:923 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" msgid "No book selected"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:905 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1001
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:923 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1019
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:937 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1033
msgid "Cannot view" msgid "Cannot view"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:911 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1007
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:942 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1038
msgid "Choose the format to view" msgid "Choose the format to view"
msgstr "" 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." msgid "%s has no available formats."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:976 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1072
msgid "Cannot configure" msgid "Cannot configure"
msgstr "" 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." msgid "Cannot configure while there are running jobs."
msgstr "" 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 " msgid "Copying database to "
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1014 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1110
msgid "Invalid database" msgid "Invalid database"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1015 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1111
msgid "" msgid ""
"<p>An invalid database already exists at %s, delete it before trying to move " "<p>An invalid database already exists at %s, delete it before trying to move "
"the existing database.<br>Error: %s" "the existing database.<br>Error: %s"
msgstr "" 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" msgid "Could not move database"
msgstr "" 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" msgid "No detailed info available"
msgstr "" 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." msgid "No detailed information is available for books on the device."
msgstr "" 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" msgid "Error talking to device"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1088 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1184
msgid "" msgid ""
"There was a temporary error talking to the device. Please unplug and " "There was a temporary error talking to the device. Please unplug and "
"reconnect the device and or reboot." "reconnect the device and or reboot."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1139 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1235
msgid "Conversion Error" msgid "Conversion Error"
msgstr "" 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" msgid "Database does not exist"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1158 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1254
msgid "" msgid ""
"The directory in which the database should be: %s no longer exists. Please " "The directory in which the database should be: %s no longer exists. Please "
"choose a new database location." "choose a new database location."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1209 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1305
msgid "" msgid ""
"<span style=\"color:red; font-weight:bold\">Latest version: <a " "<span style=\"color:red; font-weight:bold\">Latest version: <a "
"href=\"%s\">%s</a></span>" "href=\"%s\">%s</a></span>"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1215 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1311
msgid "" msgid ""
"%s has been updated to version %s. See the <a " "%s has been updated to version %s. See the <a "
"href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features</a>. " "href=\"http://calibre.kovidgoyal.net/wiki/Changelog\">new features</a>. "
"Visit the download page?" "Visit the download page?"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:1215 #: /home/kovid/work/calibre/src/calibre/gui2/main.py:1311
msgid "Update available" msgid "Update available"
msgstr "" msgstr ""
@ -2589,23 +2768,23 @@ msgstr ""
msgid "Custom news sources" msgid "Custom news sources"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:95 #: /home/kovid/work/calibre/src/calibre/gui2/status.py:99
msgid "Jobs:" msgid "Jobs:"
msgstr "" 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." msgid "Click to see list of active jobs."
msgstr "" 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" msgid "Click to browse books by their covers"
msgstr "" 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" msgid "Click to turn off Cover Browsing"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/status.py:138 #: /home/kovid/work/calibre/src/calibre/gui2/status.py:142
msgid "" msgid ""
"<p>Browsing books by their covers is disabled.<br>Import of pictureflow " "<p>Browsing books by their covers is disabled.<br>Import of pictureflow "
"module failed:<br>" "module failed:<br>"
@ -2851,11 +3030,11 @@ msgid ""
"For help on an individual command: %%prog command --help\n" "For help on an individual command: %%prog command --help\n"
msgstr "" 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." msgid "Could not launch worker process."
msgstr "" 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" msgid "Could not initialize the fontconfig library"
msgstr "" msgstr ""
@ -2964,9 +3143,8 @@ msgid ""
"downloads at most 2 feeds." "downloads at most 2 feeds."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:84 #: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:70
#: /home/kovid/work/calibre/src/calibre/web/feeds/main.py:88 #: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:580
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:577
msgid "Fetching feeds..." msgid "Fetching feeds..."
msgstr "" msgstr ""
@ -2995,58 +3173,58 @@ msgstr ""
msgid "\tFailed links:" msgid "\tFailed links:"
msgstr "" 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" msgid "Could not fetch article. Run with --debug to see the reason"
msgstr "" 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" msgid "Got feeds from index page"
msgstr "" 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..." msgid "Trying to download cover..."
msgstr "" 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)]..." msgid "Starting download [%d thread(s)]..."
msgstr "" 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" msgid "Feeds downloaded to %s"
msgstr "" 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" msgid "Could not download cover: %s"
msgstr "" 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" msgid "Downloading cover from %s"
msgstr "" 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" msgid "Untitled Article"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:745 #: /home/kovid/work/calibre/src/calibre/web/feeds/news.py:748
msgid "" msgid ""
"\n" "\n"
"Downloaded article %s from %s\n" "Downloaded article %s from %s\n"
"%s" "%s"
msgstr "" 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" msgid "Article downloaded: %s"
msgstr "" 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" msgid "Failed to download article: %s from %s\n"
msgstr "" 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" msgid "Article download failed: %s"
msgstr "" 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" msgid "Fetching feed"
msgstr "" 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. Manage application-wide preferences.
''' '''
import os, re, cPickle import os, re, cPickle, textwrap
from copy import deepcopy from copy import deepcopy
from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from PyQt4.QtCore import QString 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 from collections import defaultdict
if iswindows: if iswindows:
@ -21,10 +25,129 @@ if iswindows:
elif isosx: elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre') config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else: 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): 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): class Option(object):
@ -202,6 +325,15 @@ class Config(object):
raise IOError('Could not lock config file: %s'%self.config_file_path) raise IOError('Could not lock config file: %s'%self.config_file_path)
return self.option_set.parse_string(src) 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): def set(self, name, val):
if not self.option_set.has_option(name): if not self.option_set.has_option(name):
raise ValueError('The option %s is not defined.'%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) footer = self.option_set.get_override_section(self.src)
self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' 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__': 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 Get command line arguments as unicode objects. Note that the
first argument will be the path to the interpreter, *not* the first argument will be the path to the interpreter, *not* the
script being run. So to replace sys.argv, you should use 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. # Create all upper directories if necessary.
upperdirs = os.path.dirname(targetpath) 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): if upperdirs and not os.path.exists(upperdirs):
os.makedirs(upperdirs) os.makedirs(upperdirs)
source = self.open(member, pwd=pwd) source = self.open(member, pwd=pwd)
target = file(targetpath, "wb") target = open(targetpath, "wb")
shutil.copyfileobj(source, target) shutil.copyfileobj(source, target)
source.close() source.close()
target.close() target.close()

View File

@ -12,9 +12,10 @@ from urllib import url2pathname
from httplib import responses from httplib import responses
from calibre import setup_cli_handlers, browser, sanitize_file_name, \ 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.BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre.utils.config import OptionParser
class FetchError(Exception): class FetchError(Exception):
pass pass

View File

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