diff --git a/installer/linux/freeze.py b/installer/linux/freeze.py
index c381041675..a6151c4931 100644
--- a/installer/linux/freeze.py
+++ b/installer/linux/freeze.py
@@ -81,7 +81,8 @@ def freeze():
'PyQt4.QtScript.so', 'PyQt4.QtSql.so', 'PyQt4.QtTest.so', 'qt',
'glib', 'gobject']
- packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg']
+ packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg',
+ 'dateutil']
includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules]
diff --git a/installer/osx/freeze.py b/installer/osx/freeze.py
index 3ec24d3aba..dbaad72748 100644
--- a/installer/osx/freeze.py
+++ b/installer/osx/freeze.py
@@ -342,6 +342,7 @@ def main():
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
'keyword', 'codeop', 'pydoc', 'readline',
'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*',
+ 'dateutil',
],
'packages' : ['PIL', 'Authorization', 'lxml'],
'excludes' : ['IPython'],
diff --git a/installer/windows/freeze.py b/installer/windows/freeze.py
index ab58fb669d..56486f6bd5 100644
--- a/installer/windows/freeze.py
+++ b/installer/windows/freeze.py
@@ -179,7 +179,8 @@ def main(args=sys.argv):
'calibre.ebooks.lrf.fonts.prs500.*',
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
],
- 'packages' : ['PIL', 'lxml', 'cherrypy'],
+ 'packages' : ['PIL', 'lxml', 'cherrypy',
+ 'dateutil'],
'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk"
],
diff --git a/src/calibre/constants.py b/src/calibre/constants.py
index cbb7fba14e..2d77964693 100644
--- a/src/calibre/constants.py
+++ b/src/calibre/constants.py
@@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
-__version__ = '0.4.135'
+__version__ = '0.4.136'
__author__ = "Kovid Goyal "
'''
Various run time constants.
diff --git a/src/calibre/devices/prs505/books.py b/src/calibre/devices/prs505/books.py
index b63b089fdd..06d205fb02 100644
--- a/src/calibre/devices/prs505/books.py
+++ b/src/calibre/devices/prs505/books.py
@@ -186,7 +186,10 @@ class BookList(_BookList):
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name.rpartition('.')[-1].lower()]
cid = self.max_id()+1
- sourceid = str(self[0].sourceid) if len(self) else "1"
+ try:
+ sourceid = str(self[0].sourceid) if len(self) else '1'
+ except:
+ sourceid = '1'
attrs = {
"title" : info["title"],
'titleSorter' : sortable_title(info['title']),
diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py
index 9308af2c5a..fb8bbf5378 100644
--- a/src/calibre/devices/prs505/driver.py
+++ b/src/calibre/devices/prs505/driver.py
@@ -248,15 +248,20 @@ class PRS505(Device):
time.sleep(3)
self.open_osx()
if self._card_prefix is not None:
- cachep = os.path.join(self._card_prefix, self.CACHE_XML)
- if not os.path.exists(cachep):
- os.makedirs(os.path.dirname(cachep), mode=0777)
- f = open(cachep, 'wb')
- f.write(u'''
+ try:
+ cachep = os.path.join(self._card_prefix, self.CACHE_XML)
+ if not os.path.exists(cachep):
+ os.makedirs(os.path.dirname(cachep), mode=0777)
+ f = open(cachep, 'wb')
+ f.write(u'''
'''.encode('utf8'))
- f.close()
+ f.close()
+ except:
+ self._card_prefix = None
+ import traceback
+ traceback.print_exc()
def set_progress_reporter(self, pr):
self.report_progress = pr
diff --git a/src/calibre/ebooks/epub/from_html.py b/src/calibre/ebooks/epub/from_html.py
index bd9b59cfbd..fd94c9ee69 100644
--- a/src/calibre/ebooks/epub/from_html.py
+++ b/src/calibre/ebooks/epub/from_html.py
@@ -74,7 +74,9 @@ def check_links(opf_path, pretty_print):
html_files = []
for item in opf.itermanifest():
if 'html' in item.get('media-type', '').lower():
- f = item.get('href').split('/')[-1].decode('utf-8')
+ f = item.get('href').split('/')[-1]
+ if isinstance(f, str):
+ f = f.decode('utf-8')
html_files.append(os.path.abspath(content(f)))
for path in html_files:
diff --git a/src/calibre/ebooks/html.py b/src/calibre/ebooks/html.py
index e264fec7cb..f69f26a1e6 100644
--- a/src/calibre/ebooks/html.py
+++ b/src/calibre/ebooks/html.py
@@ -330,7 +330,8 @@ class PreProcessor(object):
sanitize_head),
# Convert all entities, since lxml doesn't handle them well
(re.compile(r'&(\S+?);'), convert_entities),
-
+ # Remove the ]*>'), lambda match: ''),
]
# Fix pdftohtml markup
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index eabd082142..d9b0514362 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -192,7 +192,8 @@ class MetaInformation(object):
for attr in ('author_sort', 'title_sort', 'comments', 'category',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
- 'manifest', 'spine', 'toc', 'cover', 'language', 'book_producer'):
+ 'manifest', 'spine', 'toc', 'cover', 'language',
+ 'book_producer', 'timestamp'):
if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr))
@@ -217,7 +218,7 @@ class MetaInformation(object):
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
- 'book_producer',
+ 'book_producer', 'timestamp'
):
setattr(self, x, getattr(mi, x, None))
@@ -235,7 +236,8 @@ class MetaInformation(object):
for attr in ('author_sort', 'title_sort', 'comments', 'category',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'application_id', 'manifest', 'spine', 'toc',
- 'cover', 'language', 'guide', 'book_producer'):
+ 'cover', 'language', 'guide', 'book_producer',
+ 'timestamp'):
if hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:
@@ -276,6 +278,8 @@ class MetaInformation(object):
ans += u'Series : '+unicode(self.series) + ' #%s\n'%self.format_series_index()
if self.language:
ans += u'Language : ' + unicode(self.language) + u'\n'
+ if self.timestamp is not None:
+ ans += u'Timestamp : ' + self.timestamp.isoformat(' ')
return ans.strip()
def to_html(self):
@@ -289,12 +293,12 @@ class MetaInformation(object):
if self.series:
ans += [(_('Series'), unicode(self.series)+ ' #%s'%self.format_series_index())]
ans += [(_('Language'), unicode(self.language))]
+ if self.timestamp is not None:
+ ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
for i, x in enumerate(ans):
ans[i] = u'%s | %s |
'%x
return u''%u'\n'.join(ans)
-
-
def __str__(self):
return self.__unicode__().encode('utf-8')
diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py
index 8cc1282067..ef41d5e937 100644
--- a/src/calibre/ebooks/metadata/library_thing.py
+++ b/src/calibre/ebooks/metadata/library_thing.py
@@ -43,8 +43,9 @@ def cover_from_isbn(isbn, timeout=5.):
src = browser.open('http://www.librarything.com/isbn/'+isbn).read().decode('utf-8', 'replace')
except Exception, err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
- raise LibraryThingError(_('LibraryThing.com timed out. Try again later.'))
- raise
+ err = LibraryThingError(_('LibraryThing.com timed out. Try again later.'))
+ raise err
+ else:
s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'})
if url is None:
diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py
index d8116b33d3..1241238f26 100644
--- a/src/calibre/ebooks/metadata/meta.py
+++ b/src/calibre/ebooks/metadata/meta.py
@@ -31,8 +31,14 @@ def metadata_from_formats(formats):
mi = MetaInformation(None, None)
formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)],
METADATA_PRIORITIES[path_to_ext(y)]))
- for path in formats:
- ext = path_to_ext(path)
+ extensions = list(map(path_to_ext, formats))
+ if 'opf' in extensions:
+ opf = formats[extensions.index('opf')]
+ mi2 = opf_metadata(opf)
+ if mi2 is not None and mi2.title:
+ return mi2
+
+ for path, ext in zip(formats, extensions):
stream = open(path, 'rb')
try:
mi.smart_update(get_metadata(stream, stream_type=ext, use_libprs_metadata=True))
diff --git a/src/calibre/ebooks/metadata/opf.xml b/src/calibre/ebooks/metadata/opf.xml
index 94a8f63b3c..9dab4efbf4 100644
--- a/src/calibre/ebooks/metadata/opf.xml
+++ b/src/calibre/ebooks/metadata/opf.xml
@@ -10,7 +10,7 @@
${author}
${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]
${mi.application_id}
-
+ ${mi.timestamp.isoformat()}
${mi.language if mi.language else 'UND'}
${mi.category}
${mi.comments}
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 32ba2cb45a..2e3b5ff047 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -12,6 +12,7 @@ from urllib import unquote
from urlparse import urlparse
from lxml import etree
+from dateutil import parser
from calibre.ebooks.chardet import xml_to_unicode
from calibre import relpath
@@ -436,6 +437,7 @@ class OPF(object):
series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1)
rating = MetadataField('rating', is_dc=False, formatter=int)
+ timestamp = MetadataField('date', formatter=parser.parse)
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True):
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index 6811f9ccda..bfbe8f5ae5 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -308,8 +308,11 @@ class MobiReader(object):
if 'filepos-id' in attrib:
attrib['id'] = attrib.pop('filepos-id')
if 'filepos' in attrib:
- filepos = int(attrib.pop('filepos'))
- attrib['href'] = "#filepos%d" % filepos
+ filepos = attrib.pop('filepos')
+ try:
+ attrib['href'] = "#filepos%d" % int(filepos)
+ except:
+ attrib['href'] = filepos
if tag.tag == 'img':
recindex = None
for attr in self.IMAGE_ATTRS:
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 084b352f48..c33036e183 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -1,7 +1,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
""" The GUI """
-import sys, os, re, StringIO, traceback
+import sys, os, re, StringIO, traceback, time
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
QModelIndex
@@ -14,6 +14,9 @@ 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
+from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
+from calibre.ebooks.metadata import MetaInformation
+
NONE = QVariant() #: Null value to return from the data function of item models
@@ -148,7 +151,41 @@ class Dispatcher(QObject):
def dispatch(self, args, kwargs):
self.func(*args, **kwargs)
+
+class GetMetadata(QObject):
+ '''
+ Convenience class to ensure that metadata readers are used only in the
+ GUI thread. Must be instantiated in the GUI thread.
+ '''
+
+ def __init__(self):
+ QObject.__init__(self)
+ self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
+ self._get_metadata, Qt.QueuedConnection)
+ self.connect(self, SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
+ self._from_formats, Qt.QueuedConnection)
+ def __call__(self, id, *args, **kwargs):
+ self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
+ id, args, kwargs)
+
+ def from_formats(self, id, *args, **kwargs):
+ self.emit(SIGNAL('idispatch(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
+ id, args, kwargs)
+
+ def _from_formats(self, id, args, kwargs):
+ try:
+ mi = metadata_from_formats(*args, **kwargs)
+ except:
+ mi = MetaInformation('', [_('Unknown')])
+ self.emit(SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'), id, mi)
+
+ def _get_metadata(self, id, args, kwargs):
+ try:
+ mi = get_metadata(*args, **kwargs)
+ except:
+ mi = MetaInformation('', [_('Unknown')])
+ self.emit(SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'), id, mi)
class TableView(QTableView):
def __init__(self, parent):
diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py
new file mode 100644
index 0000000000..a6c6c699e8
--- /dev/null
+++ b/src/calibre/gui2/add.py
@@ -0,0 +1,224 @@
+'''
+UI for adding books to the database
+'''
+import os
+
+from PyQt4.Qt import QThread, SIGNAL, QMutex, QWaitCondition, Qt
+
+from calibre.gui2.dialogs.progress import ProgressDialog
+from calibre.constants import preferred_encoding
+from calibre.gui2.widgets import WarningDialog
+
+class Add(QThread):
+
+ def __init__(self):
+ QThread.__init__(self)
+ self._lock = QMutex()
+ self._waiting = QWaitCondition()
+
+ def is_canceled(self):
+ if self.pd.canceled:
+ self.canceled = True
+ return self.canceled
+
+ def wait_for_condition(self):
+ self._lock.lock()
+ self._waiting.wait(self._lock)
+ self._lock.unlock()
+
+ def wake_up(self):
+ self._waiting.wakeAll()
+
+class AddFiles(Add):
+
+ def __init__(self, paths, default_thumbnail, get_metadata, db=None):
+ Add.__init__(self)
+ self.paths = paths
+ self.get_metadata = get_metadata
+ self.default_thumbnail = default_thumbnail
+ self.db = db
+ self.formats, self.metadata, self.names, self.infos = [], [], [], []
+ self.duplicates = []
+ self.number_of_books_added = 0
+ self.connect(self.get_metadata,
+ SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'),
+ self.metadata_delivered)
+
+ def metadata_delivered(self, id, mi):
+ if self.is_canceled():
+ self.reading.wakeAll()
+ return
+ if not mi.title:
+ mi.title = os.path.splitext(self.names[id])[0]
+ mi.title = mi.title if isinstance(mi.title, unicode) else \
+ mi.title.decode(preferred_encoding, 'replace')
+ self.metadata.append(mi)
+ self.infos.append({'title':mi.title,
+ 'authors':', '.join(mi.authors),
+ 'cover':self.default_thumbnail, 'tags':[]})
+ if self.db is not None:
+ duplicates, num = self.db.add_books(self.paths[id:id+1],
+ self.formats[id:id+1], [mi],
+ add_duplicates=False)
+ self.number_of_books_added += num
+ if duplicates:
+ if not self.duplicates:
+ self.duplicates = [[], [], [], []]
+ for i in range(4):
+ self.duplicates[i] += duplicates[i]
+ self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
+ mi.title, id)
+ self.wake_up()
+
+ def create_progress_dialog(self, title, msg, parent):
+ self._parent = parent
+ self.pd = ProgressDialog(title, msg, -1, len(self.paths)-1, parent)
+ self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
+ self.update_progress_dialog)
+ self.pd.setModal(True)
+ self.pd.show()
+ self.connect(self, SIGNAL('finished()'), self.pd.hide)
+ return self.pd
+
+
+ def update_progress_dialog(self, title, count):
+ self.pd.set_value(count)
+ if self.db is not None:
+ self.pd.set_msg(_('Added %s to library')%title)
+ else:
+ self.pd.set_msg(_('Read metadata from ')+title)
+
+
+ def run(self):
+ self.canceled = False
+ for c, book in enumerate(self.paths):
+ if self.pd.canceled:
+ self.canceled = True
+ break
+ format = os.path.splitext(book)[1]
+ format = format[1:] if format else None
+ stream = open(book, 'rb')
+ self.formats.append(format)
+ self.names.append(os.path.basename(book))
+ self.get_metadata(c, stream, stream_type=format,
+ use_libprs_metadata=True)
+ self.wait_for_condition()
+
+
+ def process_duplicates(self):
+ if self.duplicates:
+ files = _('Books with the same title as the following already '
+ 'exist in the database. Add them anyway?
')
+ for mi in self.duplicates[2]:
+ files += '- '+mi.title+'
\n'
+ d = WarningDialog (_('Duplicates found!'),
+ _('Duplicates found!'),
+ files+'
', parent=self._parent)
+ if d.exec_() == d.Accepted:
+ num = self.db.add_books(*self.duplicates,
+ **dict(add_duplicates=True))[1]
+ self.number_of_books_added += num
+
+
+class AddRecursive(Add):
+
+ def __init__(self, path, db, get_metadata, single_book_per_directory, parent):
+ self.path = path
+ self.db = db
+ self.get_metadata = get_metadata
+ self.single_book_per_directory = single_book_per_directory
+ self.duplicates, self.books, self.metadata = [], [], []
+ self.number_of_books_added = 0
+ self.canceled = False
+ Add.__init__(self)
+ self.connect(self.get_metadata,
+ SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'),
+ self.metadata_delivered, Qt.QueuedConnection)
+ self.connect(self, SIGNAL('searching_done()'), self.searching_done,
+ Qt.QueuedConnection)
+ self._parent = parent
+ self.pd = ProgressDialog(_('Adding books recursively...'),
+ _('Searching for books in all sub-directories...'),
+ 0, 0, parent)
+ self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
+ self.update_progress_dialog)
+ self.connect(self, SIGNAL('update(PyQt_PyObject)'), self.pd.set_msg,
+ Qt.QueuedConnection)
+ self.connect(self, SIGNAL('pupdate(PyQt_PyObject)'), self.pd.set_value,
+ Qt.QueuedConnection)
+ self.pd.setModal(True)
+ self.pd.show()
+ self.connect(self, SIGNAL('finished()'), self.pd.hide)
+
+ def update_progress_dialog(self, title, count):
+ self.pd.set_value(count)
+ if title:
+ self.pd.set_msg(_('Read metadata from ')+title)
+
+ def metadata_delivered(self, id, mi):
+ if self.is_canceled():
+ self.reading.wakeAll()
+ return
+ self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
+ mi.title, id)
+ self.metadata.append((mi if mi.title else None, self.books[id]))
+ if len(self.metadata) >= len(self.books):
+ self.metadata = [x for x in self.metadata if x[0] is not None]
+ self.pd.set_min(-1)
+ self.pd.set_max(len(self.metadata)-1)
+ self.pd.set_value(-1)
+ self.pd.set_msg(_('Adding books to database...'))
+ self.wake_up()
+
+ def searching_done(self):
+ self.pd.set_min(-1)
+ self.pd.set_max(len(self.books)-1)
+ self.pd.set_value(-1)
+ self.pd.set_msg(_('Reading metadata...'))
+
+
+ def run(self):
+ root = os.path.abspath(self.path)
+ for dirpath in os.walk(root):
+ if self.is_canceled():
+ return
+ self.emit(SIGNAL('update(PyQt_PyObject)'),
+ _('Searching in')+' '+dirpath[0])
+ self.books += list(self.db.find_books_in_directory(dirpath[0],
+ self.single_book_per_directory))
+ self.books = [formats for formats in self.books if formats]
+ # Reset progress bar
+ self.emit(SIGNAL('searching_done()'))
+
+ for c, formats in enumerate(self.books):
+ self.get_metadata.from_formats(c, formats)
+ self.wait_for_condition()
+
+ # Add books to database
+ for c, x in enumerate(self.metadata):
+ mi, formats = x
+ if self.is_canceled():
+ break
+ if self.db.has_book(mi):
+ self.duplicates.append((mi, formats))
+ else:
+ self.db.import_book(mi, formats, notify=False)
+ self.number_of_books_added += 1
+ self.emit(SIGNAL('pupdate(PyQt_PyObject)'), c)
+
+
+ def process_duplicates(self):
+ if self.duplicates:
+ files = _('Books with the same title as the following already '
+ 'exist in the database. Add them anyway?
')
+ for mi in self.duplicates:
+ files += '- '+mi[0].title+'
\n'
+ d = WarningDialog (_('Duplicates found!'),
+ _('Duplicates found!'),
+ files+'
', parent=self._parent)
+ if d.exec_() == d.Accepted:
+ for mi, formats in self.duplicates:
+ self.db.import_book(mi, formats, notify=False)
+ self.number_of_books_added += 1
+
+
\ No newline at end of file
diff --git a/src/calibre/gui2/dialogs/confirm_delete.py b/src/calibre/gui2/dialogs/confirm_delete.py
index 08db53e9a7..8c496987fb 100644
--- a/src/calibre/gui2/dialogs/confirm_delete.py
+++ b/src/calibre/gui2/dialogs/confirm_delete.py
@@ -5,7 +5,7 @@ __docformat__ = 'restructuredtext en'
from calibre.gui2 import dynamic
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
-from PyQt4.Qt import QDialog, SIGNAL
+from PyQt4.Qt import QDialog, SIGNAL, Qt
def _config_name(name):
return name + '_again'
@@ -19,6 +19,7 @@ class Dialog(QDialog, Ui_Dialog):
self.msg.setText(msg)
self.name = name
self.connect(self.again, SIGNAL('stateChanged(int)'), self.toggle)
+ self.buttonBox.setFocus(Qt.OtherFocusReason)
def toggle(self, x):
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 385e105c3a..78415f3a19 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -113,6 +113,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def set_cover(self):
row = self.formats.currentRow()
fmt = self.formats.item(row)
+ if fmt is None:
+ error_dialog(self, _('No format selected'),
+ _('No format selected')).exec_()
+ return
ext = fmt.ext.lower()
if fmt.path is None:
stream = self.db.format(self.row, ext, as_file=True)
@@ -121,7 +125,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
try:
mi = get_metadata(stream, ext)
except:
- error_dialog(self, _('Could not read metadata'),
+ error_dialog(self, _('Could not read metadata'),
_('Could not read metadata from %s format')%ext).exec_()
return
cdata = None
diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py
index 2543cefb4d..0f64d7b041 100644
--- a/src/calibre/gui2/dialogs/progress.py
+++ b/src/calibre/gui2/dialogs/progress.py
@@ -20,6 +20,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.setWindowModality(Qt.ApplicationModal)
self.set_min(min)
self.set_max(max)
+ self.bar.setValue(min)
self.canceled = False
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled)
diff --git a/src/calibre/gui2/images/document-print-preview.svg b/src/calibre/gui2/images/document-print-preview.svg
deleted file mode 100644
index 6ffe4fafa8..0000000000
--- a/src/calibre/gui2/images/document-print-preview.svg
+++ /dev/null
@@ -1,14298 +0,0 @@
-
-
-
diff --git a/src/calibre/gui2/images/document-print.svg b/src/calibre/gui2/images/document-print.svg
deleted file mode 100644
index dffa8b94ba..0000000000
--- a/src/calibre/gui2/images/document-print.svg
+++ /dev/null
@@ -1,14229 +0,0 @@
-
-
-
diff --git a/src/calibre/gui2/images/news/pobjeda.png b/src/calibre/gui2/images/news/pobjeda.png
new file mode 100644
index 0000000000..d7612b4e9e
Binary files /dev/null and b/src/calibre/gui2/images/news/pobjeda.png differ
diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py
index 9f0877ca09..199c4ada67 100644
--- a/src/calibre/gui2/library.py
+++ b/src/calibre/gui2/library.py
@@ -1,3 +1,4 @@
+from calibre.ebooks.metadata import authors_to_string
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
import os, textwrap, traceback, time, re
@@ -371,35 +372,24 @@ class BooksModel(QAbstractTableModel):
if not rows_are_ids:
rows = [self.db.id(row.row()) for row in rows]
for id in rows:
- au = self.db.authors(id, index_is_id=True)
- tags = self.db.tags(id, index_is_id=True)
- if not au:
- au = _('Unknown')
- au = au.split(',')
- if len(au) > 1:
- t = ', '.join(au[:-1])
- t += ' & ' + au[-1]
- au = t
- else:
- au = ' & '.join(au)
- if not tags:
- tags = []
- else:
- tags = tags.split(',')
- series = self.db.series(id, index_is_id=True)
- if series is not None:
- tags.append(series)
- mi = {
- 'title' : self.db.title(id, index_is_id=True),
+ mi = self.db.get_metadata(id, index_is_id=True)
+ au = authors_to_string(mi.authors if mi.authors else [_('Unknown')])
+ tags = mi.tags if mi.tags else []
+ if mi.series is not None:
+ tags.append(mi.series)
+ info = {
+ 'title' : mi.title,
'authors' : au,
'cover' : self.db.cover(id, index_is_id=True),
'tags' : tags,
- 'comments': self.db.comments(id, index_is_id=True),
+ 'comments': mi.comments,
}
- if series is not None:
- mi['tag order'] = {series:self.db.books_in_series_of(id, index_is_id=True)}
+ if mi.series is not None:
+ info['tag order'] = {
+ mi.series:self.db.books_in_series_of(id, index_is_id=True)
+ }
- metadata.append(mi)
+ metadata.append(info)
return metadata
def get_preferred_formats_from_ids(self, ids, all_formats, mode='r+b'):
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index 3649a2264b..83665ac8a7 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -13,7 +13,6 @@ from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
iswindows, isosx, preferred_encoding
from calibre.ptempfile import PersistentTemporaryFile
-from calibre.ebooks.metadata.meta import get_metadata
from calibre.devices.errors import FreeSpaceError
from calibre.devices.interface import Device
from calibre.utils.config import prefs, dynamic
@@ -23,7 +22,7 @@ from calibre.gui2 import APP_UID, warning_dialog, choose_files, error_dialog, \
set_sidebar_directories, Dispatcher, \
SingleApplication, Application, available_height, \
max_available_height, config, info_dialog, \
- available_width
+ available_width, GetMetadata
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror
from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.gui2.update import CheckForUpdates
@@ -49,7 +48,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.parallel import JobKilled
from calibre.utils.filenames import ascii_filename
-from calibre.gui2.widgets import WarningDialog
from calibre.gui2.dialogs.confirm_delete import confirm
class Main(MainWindow, Ui_MainWindow):
@@ -78,6 +76,7 @@ class Main(MainWindow, Ui_MainWindow):
self.setupUi(self)
self.setWindowTitle(__appname__)
self.verbose = opts.verbose
+ self.get_metadata = GetMetadata()
self.read_settings()
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
@@ -391,7 +390,7 @@ class Main(MainWindow, Ui_MainWindow):
def change_output_format(self, x):
of = unicode(x).strip()
if of != prefs['output_format']:
- if of not in ('LRF',):
+ if of not in ('LRF', 'EPUB'):
warning_dialog(self, 'Warning',
'%s support is still in beta. If you find bugs, please report them by opening a ticket.'%of).exec_()
prefs.set('output_format', of)
@@ -608,36 +607,26 @@ class Main(MainWindow, Ui_MainWindow):
################################# Add books ################################
def add_recursive(self, single):
- root = choose_dir(self, 'recursive book import root dir dialog', 'Select root folder')
+ root = choose_dir(self, 'recursive book import root dir dialog',
+ 'Select root folder')
if not root:
return
- progress = ProgressDialog(_('Adding books recursively...'),
- min=0, max=0, parent=self)
- progress.show()
- def callback(msg):
- if msg != '.':
- progress.set_msg((_('Added ')+msg) if msg else _('Searching...'))
- QApplication.processEvents()
- QApplication.sendPostedEvents()
- QApplication.flush()
- return progress.canceled
- try:
- duplicates = self.library_view.model().db.recursive_import(root, single, callback=callback)
- finally:
- progress.hide()
- if duplicates:
- files = _('
Books with the same title as the following already exist in the database. Add them anyway?
')
- for mi, formats in duplicates:
- files += '- '+mi.title+'
\n'
- d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'),
- files+'
', self)
- if d.exec_() == QDialog.Accepted:
- for mi, formats in duplicates:
- self.library_view.model().db.import_book(mi, formats )
-
- self.library_view.model().resort()
- self.library_view.model().research()
-
+ from calibre.gui2.add import AddRecursive
+ self._add_recursive_thread = AddRecursive(root,
+ self.library_view.model().db, self.get_metadata,
+ single, self)
+ self.connect(self._add_recursive_thread, SIGNAL('finished()'),
+ self._recursive_files_added)
+ self._add_recursive_thread.start()
+
+ def _recursive_files_added(self):
+ self._add_recursive_thread.process_duplicates()
+ if self._add_recursive_thread.number_of_books_added > 0:
+ self.library_view.model().resort(reset=False)
+ self.library_view.model().research()
+ self.library_view.model().count_changed()
+ self._add_recursive_thread = None
+
def add_recursive_single(self, checked):
'''
Add books from the local filesystem to either the library or the device
@@ -686,66 +675,41 @@ class Main(MainWindow, Ui_MainWindow):
return
to_device = self.stack.currentIndex() != 0
self._add_books(books, to_device)
- if to_device:
- self.status_bar.showMessage(_('Uploading books to device.'), 2000)
+
def _add_books(self, paths, to_device, on_card=None):
if on_card is None:
on_card = self.stack.currentIndex() == 2
if not paths:
return
- # Get format and metadata information
- formats, metadata, names, infos = [], [], [], []
- progress = ProgressDialog(_('Adding books...'), _('Reading metadata...'),
- min=0, max=len(paths), parent=self)
- progress.show()
- try:
- for c, book in enumerate(paths):
- progress.set_value(c+1)
- if progress.canceled:
- return
- format = os.path.splitext(book)[1]
- format = format[1:] if format else None
- stream = open(book, 'rb')
- try:
- mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
- except:
- mi = MetaInformation(None, None)
- if not mi.title:
- mi.title = os.path.splitext(os.path.basename(book))[0]
- if not mi.authors:
- mi.authors = [_('Unknown')]
- formats.append(format)
- metadata.append(mi)
- names.append(os.path.basename(book))
- infos.append({'title':mi.title, 'authors':', '.join(mi.authors),
- 'cover':self.default_thumbnail, 'tags':[]})
- title = mi.title if isinstance(mi.title, unicode) else mi.title.decode(preferred_encoding, 'replace')
- progress.set_msg(_('Read metadata from ')+title)
- QApplication.processEvents()
-
- if not to_device:
- progress.set_msg(_('Adding books to database...'))
- QApplication.processEvents()
- model = self.library_view.model()
-
- paths = list(paths)
- duplicates, number_added = model.add_books(paths, formats, metadata)
- if duplicates:
- files = _('Books with the same title as the following already exist in the database. Add them anyway?
')
- for mi in duplicates[2]:
- files += '- '+mi.title+'
\n'
- d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'
', parent=self)
- if d.exec_() == QDialog.Accepted:
- num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
- number_added += num
- model.books_added(number_added)
+ from calibre.gui2.add import AddFiles
+ self._add_files_thread = AddFiles(paths, self.default_thumbnail,
+ self.get_metadata,
+ None if to_device else \
+ self.library_view.model().db
+ )
+ self._add_files_thread.send_to_device = to_device
+ self._add_files_thread.on_card = on_card
+ self._add_files_thread.create_progress_dialog(_('Adding books...'),
+ _('Reading metadata...'), self)
+ self.connect(self._add_files_thread, SIGNAL('finished()'),
+ self._files_added)
+ self._add_files_thread.start()
+
+ def _files_added(self):
+ t = self._add_files_thread
+ self._add_files_thread = None
+ if not t.canceled:
+ if t.send_to_device:
+ self.upload_books(t.paths,
+ list(map(sanitize_file_name, t.names)),
+ t.infos, on_card=t.on_card)
+ self.status_bar.showMessage(_('Uploading books to device.'), 2000)
else:
- self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
- finally:
- QApplication.processEvents()
- progress.hide()
-
+ t.process_duplicates()
+ if t.number_of_books_added > 0:
+ self.library_view.model().books_added(t.number_of_books_added)
+
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
Upload books to device.
@@ -801,7 +765,10 @@ class Main(MainWindow, Ui_MainWindow):
if not rows or len(rows) == 0:
return
if self.stack.currentIndex() == 0:
- if not confirm(''+_('The selected books will be permanently deleted and the files removed from your computer. Are you sure?')+'
', 'library_delete_books', self):
+ if not confirm(''+_('The selected books will be '
+ 'permanently deleted and the files '
+ 'removed from your computer. Are you sure?')
+ +'
', 'library_delete_books', self):
return
view.model().delete_books(rows)
else:
@@ -1410,8 +1377,15 @@ class Main(MainWindow, Ui_MainWindow):
def initialize_database(self):
self.library_path = prefs['library_path']
if self.library_path is None: # Need to migrate to new database layout
+ base = os.path.expanduser('~')
+ if iswindows:
+ from calibre import plugins
+ from PyQt4.Qt import QDir
+ base = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_PERSONAL)
+ if not base or not os.path.exists(base):
+ base = unicode(QDir.homePath()).replace('/', os.sep)
dir = unicode(QFileDialog.getExistingDirectory(self,
- _('Choose a location for your ebook library.'), os.getcwd()))
+ _('Choose a location for your ebook library.'), base))
if not dir:
dir = os.path.expanduser('~/Library')
self.library_path = os.path.abspath(dir)
@@ -1597,6 +1571,11 @@ def main(args=sys.argv):
print 'Restarting with:', e, sys.argv
os.execvp(e, sys.argv)
else:
+ if iswindows:
+ try:
+ main.system_tray_icon.hide()
+ except:
+ pass
return ret
return 0
diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py
index b6885c38e9..3745036249 100644
--- a/src/calibre/gui2/viewer/documentview.py
+++ b/src/calibre/gui2/viewer/documentview.py
@@ -8,8 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, math, re
from PyQt4.Qt import QWidget, QSize, QSizePolicy, QUrl, SIGNAL, Qt, QTimer, \
QPainter, QPalette, QBrush, QFontDatabase, QDialog, \
- QByteArray, QColor, QWheelEvent, QPoint, QImage, QRegion, \
- QFont, QPrinter, QPrintPreviewDialog, QPrintDialog
+ QByteArray, QColor, QWheelEvent, QPoint, QImage, QRegion, QFont
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.utils.config import Config, StringConfig
@@ -310,61 +309,6 @@ class DocumentView(QWebView):
def goto_bookmark(self, bm):
self.document.goto_bookmark(bm)
-
- def all_content(self):
- book_content = ''
-
- if self.manager is not None:
- for path in self.manager.iterator.spine:
- html = open(path, 'rb').read().decode(path.encoding)
- book_content += EntityDeclarationProcessor(html).processed_html
- base_url = QUrl.fromLocalFile(self.manager.iterator.spine[0])
- else:
- book_content = self.page().mainFrame().toHtml()
- base_url = QUrl.fromLocalFile(self.path())
-
- return (book_content, base_url)
-
- def print_preview(self):
- print_view = QWebView()
- book_content, base_url = self.all_content()
- print_view.setHtml(book_content, base_url)
- print_view.setTextSizeMultiplier(self.textSizeMultiplier())
-
- def finished(ok):
- printer = QPrinter(QPrinter.HighResolution)
- printer.setPageMargins(1, 1, 1, 1, QPrinter.Inch)
-
- previewDialog = QPrintPreviewDialog(printer, self)
-
- self.connect(previewDialog, SIGNAL('paintRequested(QPrinter *)'), print_view.print_)
- previewDialog.exec_()
- self.disconnect(previewDialog, SIGNAL('paintRequested(QPrinter *)'), print_view.print_)
-
- self.disconnect(print_view, SIGNAL('loadFinished(bool)'), finished)
-
- self.connect(print_view, SIGNAL('loadFinished(bool)'), finished)
-
- def print_book(self):
- print_view = QWebView()
- book_content, base_url = self.all_content()
- print_view.setHtml(book_content, base_url)
- print_view.setTextSizeMultiplier(self.textSizeMultiplier())
-
- def finished(ok):
- printer = QPrinter(QPrinter.HighResolution)
- printer.setPageMargins(1, 1, 1, 1, QPrinter.Inch)
-
- printDialog = QPrintDialog(printer, self)
- printDialog.setWindowTitle(_("Print eBook"))
-
- printDialog.exec_()
- if printDialog.result() == QDialog.Accepted:
- print_view.print_(printer)
-
- self.disconnect(print_view, SIGNAL('loadFinished(bool)'), finished)
-
- self.connect(print_view, SIGNAL('loadFinished(bool)'), finished)
def config(self, parent=None):
self.document.do_config(parent)
@@ -612,4 +556,4 @@ class DocumentView(QWebView):
self.manager.scrolled(self.scroll_fraction)
return ret
-
+
\ No newline at end of file
diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py
index 3b9c0fca1e..fedebc66d7 100644
--- a/src/calibre/gui2/viewer/main.py
+++ b/src/calibre/gui2/viewer/main.py
@@ -248,8 +248,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.connect(self.action_back, SIGNAL('triggered(bool)'), self.back)
self.connect(self.action_bookmark, SIGNAL('triggered(bool)'), self.bookmark)
self.connect(self.action_forward, SIGNAL('triggered(bool)'), self.forward)
- self.connect(self.action_print_preview, SIGNAL('triggered()'), self.view.print_preview)
- self.connect(self.action_print, SIGNAL('triggered()'), self.view.print_book)
self.connect(self.action_preferences, SIGNAL('triggered(bool)'), lambda x: self.view.config(self))
self.connect(self.pos, SIGNAL('valueChanged(double)'), self.goto_page)
self.connect(self.vertical_scrollbar, SIGNAL('valueChanged(int)'),
diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui
index 122993d528..59f813b2bd 100644
--- a/src/calibre/gui2/viewer/main.ui
+++ b/src/calibre/gui2/viewer/main.ui
@@ -27,8 +27,8 @@
-
-
-
+
+
about:blank
@@ -87,9 +87,6 @@
-
-
-
@@ -237,24 +234,6 @@
Toggle full screen
-
-
-
- :/images/document-print.svg:/images/document-print.svg
-
-
- Print
-
-
-
-
-
- :/images/document-print-preview.svg:/images/document-print-preview.svg
-
-
- Print Preview
-
-
diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py
index 37fdeb4ce4..e4fa71feed 100644
--- a/src/calibre/library/database.py
+++ b/src/calibre/library/database.py
@@ -4,13 +4,10 @@ __copyright__ = '2008, Kovid Goyal '
Backend that implements storage of ebooks in an sqlite database.
'''
import sqlite3 as sqlite
-import datetime, re, os, cPickle, sre_constants
+import datetime, re, cPickle, sre_constants
from zlib import compress, decompress
-from calibre import sanitize_file_name
-from calibre.ebooks.metadata.meta import set_metadata, metadata_from_formats
from calibre.ebooks.metadata import MetaInformation
-from calibre.ebooks import BOOK_EXTENSIONS
from calibre.web.feeds.recipes import migrate_automatic_profile_to_automatic_recipe
class Concatenate(object):
@@ -1391,117 +1388,12 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
- def import_book(self, mi, formats):
- series_index = 1 if mi.series_index is None else mi.series_index
- if not mi.authors:
- mi.authors = [_('Unknown')]
- aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
- obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
- (mi.title, None, series_index, aus))
- id = obj.lastrowid
- self.conn.commit()
- self.set_metadata(id, mi)
- for path in formats:
- ext = os.path.splitext(path)[1][1:].lower()
- stream = open(path, 'rb')
- stream.seek(0, 2)
- usize = stream.tell()
- stream.seek(0)
- data = sqlite.Binary(compress(stream.read()))
- try:
- self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)',
- (id, ext, usize, data))
- except sqlite.IntegrityError:
- self.conn.execute('UPDATE data SET uncompressed_size=?, data=? WHERE book=? AND format=?',
- (usize, data, id, ext))
- self.conn.commit()
-
- def import_book_directory_multiple(self, dirpath, callback=None):
- dirpath = os.path.abspath(dirpath)
- duplicates = []
- books = {}
- for path in os.listdir(dirpath):
- if callable(callback):
- callback('.')
- path = os.path.abspath(os.path.join(dirpath, path))
- if os.path.isdir(path) or not os.access(path, os.R_OK):
- continue
- ext = os.path.splitext(path)[1]
- if not ext:
- continue
- ext = ext[1:].lower()
- if ext not in BOOK_EXTENSIONS:
- continue
-
- key = os.path.splitext(path)[0]
- if not books.has_key(key):
- books[key] = []
-
- books[key].append(path)
-
- for formats in books.values():
- mi = metadata_from_formats(formats)
- if mi.title is None:
- continue
- if self.has_book(mi):
- duplicates.append((mi, formats))
- continue
- self.import_book(mi, formats)
- if callable(callback):
- if callback(mi.title):
- break
- return duplicates
-
-
- def import_book_directory(self, dirpath, callback=None):
- dirpath = os.path.abspath(dirpath)
- formats = []
- for path in os.listdir(dirpath):
- if callable(callback):
- callback('.')
- path = os.path.abspath(os.path.join(dirpath, path))
- if os.path.isdir(path) or not os.access(path, os.R_OK):
- continue
- ext = os.path.splitext(path)[1]
- if not ext:
- continue
- ext = ext[1:].lower()
- if ext not in BOOK_EXTENSIONS:
- continue
- formats.append(path)
-
- if not formats:
- return
-
- mi = metadata_from_formats(formats)
- if mi.title is None:
- return
- if self.has_book(mi):
- return [(mi, formats)]
- self.import_book(mi, formats)
- if callable(callback):
- callback(mi.title)
-
-
+
def has_id(self, id):
return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None
- def recursive_import(self, root, single_book_per_directory=True, callback=None):
- root = os.path.abspath(root)
- duplicates = []
- for dirpath in os.walk(root):
- res = self.import_book_directory(dirpath[0], callback=callback) if \
- single_book_per_directory else \
- self.import_book_directory_multiple(dirpath[0], callback=callback)
- if res is not None:
- duplicates.extend(res)
- if callable(callback):
- if callback(''):
- break
-
- return duplicates
-
+
class SearchToken(object):
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 8c762f8680..388a2d4fdb 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -12,20 +12,24 @@ from itertools import repeat
from datetime import datetime
from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock
-from PyQt4.QtGui import QApplication, QPixmap, QImage
+from PyQt4.QtGui import QApplication, QImage
__app = None
from calibre.library import title_sort
from calibre.library.database import LibraryDatabase
from calibre.library.sqlite import connect, IntegrityError
from calibre.utils.search_query_parser import SearchQueryParser
-from calibre.ebooks.metadata import string_to_authors, authors_to_string, MetaInformation
-from calibre.ebooks.metadata.meta import get_metadata, set_metadata
+from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
+ MetaInformation, authors_to_sort_string
+from calibre.ebooks.metadata.meta import get_metadata, set_metadata, \
+ metadata_from_formats
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import
+
from calibre import sanitize_file_name
+from calibre.ebooks import BOOK_EXTENSIONS
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
@@ -377,8 +381,10 @@ class LibraryDatabase2(LibraryDatabase):
return row[loc]
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
- 'publisher', 'rating', 'series', 'series_index', 'tags', 'title'):
- setattr(self, prop, functools.partial(get_property, loc=FIELD_MAP['comments' if prop == 'comment' else prop]))
+ 'publisher', 'rating', 'series', 'series_index', 'tags',
+ 'title', 'timestamp'):
+ setattr(self, prop, functools.partial(get_property,
+ loc=FIELD_MAP['comments' if prop == 'comment' else prop]))
def initialize_database(self):
from calibre.resources import metadata_sqlite
@@ -587,6 +593,7 @@ class LibraryDatabase2(LibraryDatabase):
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
+ mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
tags = self.tags(idx, index_is_id=index_is_id)
if tags:
mi.tags = [i.strip() for i in tags.split(',')]
@@ -627,7 +634,7 @@ class LibraryDatabase2(LibraryDatabase):
if not QCoreApplication.instance():
global __app
__app = QApplication([])
- p = QPixmap()
+ p = QImage()
if callable(getattr(data, 'read', None)):
data = data.read()
p.loadFromData(data)
@@ -881,6 +888,8 @@ class LibraryDatabase2(LibraryDatabase):
self.set_isbn(id, mi.isbn, notify=False)
if mi.series_index and mi.series_index > 0:
self.set_series_index(id, mi.series_index, notify=False)
+ if getattr(mi, 'timestamp', None) is not None:
+ self.set_timestamp(id, mi.timestamp, notify=False)
self.set_path(id, True)
self.notify('metadata', [id])
@@ -1142,7 +1151,7 @@ class LibraryDatabase2(LibraryDatabase):
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
'''
Add a book to the database. The result cache is not updated.
- @param paths: List of paths to book files or file-like objects
+ :param:`paths` List of paths to book files or file-like objects
'''
formats, metadata, uris = iter(formats), iter(metadata), iter(uris)
duplicates = []
@@ -1180,31 +1189,40 @@ class LibraryDatabase2(LibraryDatabase):
self.conn.commit()
self.data.refresh_ids(self.conn, ids) # Needed to update format list and size
if duplicates:
- paths = tuple(duplicate[0] for duplicate in duplicates)
- formats = tuple(duplicate[1] for duplicate in duplicates)
- metadata = tuple(duplicate[2] for duplicate in duplicates)
- uris = tuple(duplicate[3] for duplicate in duplicates)
+ paths = list(duplicate[0] for duplicate in duplicates)
+ formats = list(duplicate[1] for duplicate in duplicates)
+ metadata = list(duplicate[2] for duplicate in duplicates)
+ uris = list(duplicate[3] for duplicate in duplicates)
return (paths, formats, metadata, uris), len(ids)
return None, len(ids)
- def import_book(self, mi, formats):
+ def import_book(self, mi, formats, notify=True):
series_index = 1 if mi.series_index is None else mi.series_index
+ if not mi.title:
+ mi.title = _('Unknown')
if not mi.authors:
- mi.authors = ['Unknown']
- aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
+ mi.authors = [_('Unknown')]
+ aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors)
+ if isinstance(aus, str):
+ aus = aus.decode(preferred_encoding, 'replace')
+ title = mi.title if isinstance(mi.title, unicode) else \
+ mi.title.decode(preferred_encoding, 'replace')
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
- (mi.title, None, series_index, aus))
+ (title, None, series_index, aus))
id = obj.lastrowid
self.data.books_added([id], self.conn)
self.set_path(id, True)
self.set_metadata(id, mi)
for path in formats:
ext = os.path.splitext(path)[1][1:].lower()
+ if ext == 'opf':
+ continue
stream = open(path, 'rb')
self.add_format(id, ext, stream, index_is_id=True)
self.conn.commit()
self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size
- self.notify('add', [id])
+ if notify:
+ self.notify('add', [id])
def move_library_to(self, newloc, progress=None):
header = _(u'
Copying books to %s
')%newloc
@@ -1388,6 +1406,11 @@ books_series_link feeds
f = open(os.path.join(base, sanitize_file_name(name)+'.opf'), 'wb')
if not mi.authors:
mi.authors = [_('Unknown')]
+ cdata = self.cover(int(id), index_is_id=True)
+ if cdata is not None:
+ cname = sanitize_file_name(name)+'.jpg'
+ open(os.path.join(base, cname), 'wb').write(cdata)
+ mi.cover = cname
opf = OPFCreator(base, mi)
opf.render(f)
f.close()
@@ -1451,6 +1474,88 @@ books_series_link feeds
if not callback(count, title):
break
return failures
+
+ def find_books_in_directory(self, dirpath, single_book_per_directory):
+ dirpath = os.path.abspath(dirpath)
+ if single_book_per_directory:
+ formats = []
+ for path in os.listdir(dirpath):
+ path = os.path.abspath(os.path.join(dirpath, path))
+ if os.path.isdir(path) or not os.access(path, os.R_OK):
+ continue
+ ext = os.path.splitext(path)[1]
+ if not ext:
+ continue
+ ext = ext[1:].lower()
+ if ext not in BOOK_EXTENSIONS and ext != 'opf':
+ continue
+ formats.append(path)
+ yield formats
+ else:
+ books = {}
+ for path in os.listdir(dirpath):
+ path = os.path.abspath(os.path.join(dirpath, path))
+ if os.path.isdir(path) or not os.access(path, os.R_OK):
+ continue
+ ext = os.path.splitext(path)[1]
+ if not ext:
+ continue
+ ext = ext[1:].lower()
+ if ext not in BOOK_EXTENSIONS:
+ continue
+
+ key = os.path.splitext(path)[0]
+ if not books.has_key(key):
+ books[key] = []
+ books[key].append(path)
+
+ for formats in books.values():
+ yield formats
+
+ def import_book_directory_multiple(self, dirpath, callback=None):
+ duplicates = []
+ for formats in self.find_books_in_directory(dirpath, False):
+ mi = metadata_from_formats(formats)
+ if mi.title is None:
+ continue
+ if self.has_book(mi):
+ duplicates.append((mi, formats))
+ continue
+ self.import_book(mi, formats)
+ if callable(callback):
+ if callback(mi.title):
+ break
+ return duplicates
+
+ def import_book_directory(self, dirpath, callback=None):
+ dirpath = os.path.abspath(dirpath)
+ formats = self.find_books_in_directory(dirpath, True)
+ if not formats:
+ return
+
+ mi = metadata_from_formats(formats)
+ if mi.title is None:
+ return
+ if self.has_book(mi):
+ return [(mi, formats)]
+ self.import_book(mi, formats)
+ if callable(callback):
+ callback(mi.title)
+
+ def recursive_import(self, root, single_book_per_directory=True, callback=None):
+ root = os.path.abspath(root)
+ duplicates = []
+ for dirpath in os.walk(root):
+ res = self.import_book_directory(dirpath[0], callback=callback) if \
+ single_book_per_directory else \
+ self.import_book_directory_multiple(dirpath[0], callback=callback)
+ if res is not None:
+ duplicates.extend(res)
+ if callable(callback):
+ if callback(''):
+ break
+
+ return duplicates
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index 938e5e665c..cc30f6dd5c 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -12,11 +12,50 @@ from sqlite3 import IntegrityError
from threading import Thread
from Queue import Queue
from threading import RLock
+from datetime import tzinfo, datetime, timedelta
from calibre.library import title_sort
global_lock = RLock()
+def convert_timestamp(val):
+ datepart, timepart = val.split(' ')
+ tz, mult = None, 1
+ x = timepart.split('+')
+ if len(x) > 1:
+ timepart, tz = x
+ else:
+ x = timepart.split('-')
+ if len(x) > 1:
+ timepart, tz = x
+ mult = -1
+
+ year, month, day = map(int, datepart.split("-"))
+ timepart_full = timepart.split(".")
+ hours, minutes, seconds = map(int, timepart_full[0].split(":"))
+ if len(timepart_full) == 2:
+ microseconds = int(timepart_full[1])
+ else:
+ microseconds = 0
+ if tz is not None:
+ h, m = map(int, tz.split(':'))
+ delta = timedelta(minutes=mult*(60*h + m))
+ tz = type('CustomTZ', (tzinfo,), {'utcoffset':lambda self, dt:delta,
+ 'dst':lambda self,dt:timedelta(0)})()
+
+ val = datetime(year, month, day, hours, minutes, seconds, microseconds,
+ tzinfo=tz)
+ if tz is not None:
+ val = datetime(*(val.utctimetuple()[:6]))
+ return val
+
+def adapt_datetime(dt):
+ dt = datetime(*(dt.utctimetuple()[:6]))
+ return dt.isoformat(' ')
+
+sqlite.register_adapter(datetime, adapt_datetime)
+sqlite.register_converter('timestamp', convert_timestamp)
+
class Concatenate(object):
'''String concatenation aggregator for sqlite'''
def __init__(self, sep=','):
diff --git a/src/calibre/manual/templates/layout.html b/src/calibre/manual/templates/layout.html
new file mode 100644
index 0000000000..3564357684
--- /dev/null
+++ b/src/calibre/manual/templates/layout.html
@@ -0,0 +1,12 @@
+{% extends "!layout.html" %}
+{% block sidebarlogo %}
+{{ super() }}
+
+
+{% endblock %}
+
diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py
index 63bb4006e1..7abf7faed4 100644
--- a/src/calibre/trac/plugins/download.py
+++ b/src/calibre/trac/plugins/download.py
@@ -35,6 +35,7 @@ class Distribution(object):
('xdg-utils', '1.0.2', 'xdg-utils', 'xdg-utils', 'xdg-utils'),
('dbus-python', '0.82.2', 'dbus-python', 'python-dbus', 'dbus-python'),
('lxml', '2.0.5', 'lxml', 'python-lxml', 'python-lxml'),
+ ('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
('help2man', '1.36.4', 'help2man', 'help2man', 'help2man'),
]
diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py
index 60ae0761cf..dcbec14687 100644
--- a/src/calibre/web/feeds/recipes/__init__.py
+++ b/src/calibre/web/feeds/recipes/__init__.py
@@ -28,6 +28,7 @@ recipe_modules = ['recipe_' + r for r in (
'la_tercera', 'el_mercurio_chile', 'la_cuarta', 'lanacion_chile', 'la_segunda',
'jb_online', 'estadao', 'o_globo', 'vijesti', 'elmundo', 'the_oz',
'honoluluadvertiser', 'starbulletin', 'exiled', 'indy_star', 'dna',
+ 'pobjeda',
)]
import re, imp, inspect, time, os
diff --git a/src/calibre/web/feeds/recipes/recipe_pobjeda.py b/src/calibre/web/feeds/recipes/recipe_pobjeda.py
new file mode 100644
index 0000000000..9a4dbb0eee
--- /dev/null
+++ b/src/calibre/web/feeds/recipes/recipe_pobjeda.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, Darko Miletic '
+
+'''
+pobjeda.co.me
+'''
+
+import re
+from calibre import strftime
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class Pobjeda(BasicNewsRecipe):
+ title = 'Pobjeda Online'
+ __author__ = 'Darko Miletic'
+ description = 'News from Montenegro'
+ publisher = 'Pobjeda a.d.'
+ category = 'news, politics, Montenegro'
+ language = _('Serbian')
+ oldest_article = 2
+ max_articles_per_feed = 100
+ no_stylesheets = True
+ remove_javascript = True
+ encoding = 'utf8'
+ remove_javascript = True
+ use_embedded_content = False
+ INDEX = u'http://www.pobjeda.co.me'
+ extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{text-align: justify; font-family: serif1, serif} .article_description{font-family: serif1, serif}'
+
+ html2lrf_options = [
+ '--comment', description
+ , '--category', category
+ , '--publisher', publisher
+ ]
+
+ html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
+
+ preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
+
+ keep_only_tags = [dict(name='div', attrs={'class':'vijest'})]
+
+ remove_tags = [dict(name=['object','link'])]
+
+ feeds = [
+ (u'Politika' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=1' )
+ ,(u'Ekonomija' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=2' )
+ ,(u'Drustvo' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=3' )
+ ,(u'Crna Hronika' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=4' )
+ ,(u'Kultura' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=5' )
+ ,(u'Hronika Podgorice' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=7' )
+ ,(u'Feljton' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=8' )
+ ,(u'Crna Gora' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=9' )
+ ,(u'Svijet' , u'http://www.pobjeda.co.me/rubrika.php?rubrika=202')
+ ,(u'Ekonomija i Biznis', u'http://www.pobjeda.co.me/dodatak.php?rubrika=11' )
+ ,(u'Djeciji Svijet' , u'http://www.pobjeda.co.me/dodatak.php?rubrika=12' )
+ ,(u'Kultura i Drustvo' , u'http://www.pobjeda.co.me/dodatak.php?rubrika=13' )
+ ,(u'Agora' , u'http://www.pobjeda.co.me/dodatak.php?rubrika=133')
+ ,(u'Ekologija' , u'http://www.pobjeda.co.me/dodatak.php?rubrika=252')
+ ]
+
+ def preprocess_html(self, soup):
+ soup.html['xml:lang'] = 'sr-Latn-ME'
+ soup.html['lang'] = 'sr-Latn-ME'
+ mtag = ''
+ soup.head.insert(0,mtag)
+ for item in soup.findAll(style=True):
+ del item['style']
+ return soup
+
+ def get_cover_url(self):
+ cover_url = None
+ soup = self.index_to_soup(self.INDEX)
+ cover_item = soup.find('img',attrs={'alt':'Naslovna strana'})
+ if cover_item:
+ cover_url = self.INDEX + cover_item.parent['href']
+ return cover_url
+
+ def parse_index(self):
+ totalfeeds = []
+ lfeeds = self.get_feeds()
+ for feedobj in lfeeds:
+ feedtitle, feedurl = feedobj
+ self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
+ articles = []
+ soup = self.index_to_soup(feedurl)
+ for item in soup.findAll('div', attrs={'class':'vijest'}):
+ description = self.tag_to_string(item.h2)
+ atag = item.h1.find('a')
+ if atag:
+ url = self.INDEX + '/' + atag['href']
+ title = self.tag_to_string(atag)
+ date = strftime(self.timefmt)
+ articles.append({
+ 'title' :title
+ ,'date' :date
+ ,'url' :url
+ ,'description':description
+ })
+ totalfeeds.append((feedtitle, articles))
+ return totalfeeds
+
diff --git a/src/calibre/web/feeds/recipes/recipe_politika.py b/src/calibre/web/feeds/recipes/recipe_politika.py
index 1575d8984f..f1d84915ce 100644
--- a/src/calibre/web/feeds/recipes/recipe_politika.py
+++ b/src/calibre/web/feeds/recipes/recipe_politika.py
@@ -16,6 +16,7 @@ class Politika(BasicNewsRecipe):
publisher = 'Politika novine i Magazini d.o.o'
category = 'news, politics, Serbia'
oldest_article = 2
+ language = _('Serbian')
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False