mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
a89631e61d
@ -40,6 +40,7 @@ class LinuxFreeze(Command):
|
|||||||
'/usr/bin/pdftohtml',
|
'/usr/bin/pdftohtml',
|
||||||
'/usr/lib/libwmflite-0.2.so.7',
|
'/usr/lib/libwmflite-0.2.so.7',
|
||||||
'/usr/lib/liblcms.so.1',
|
'/usr/lib/liblcms.so.1',
|
||||||
|
'/usr/lib/libstlport.so.5.1',
|
||||||
'/tmp/calibre-mount-helper',
|
'/tmp/calibre-mount-helper',
|
||||||
'/usr/lib/libunrar.so',
|
'/usr/lib/libunrar.so',
|
||||||
'/usr/lib/libchm.so.0',
|
'/usr/lib/libchm.so.0',
|
||||||
|
@ -342,13 +342,6 @@ def detect_ncpus():
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def launch(path_or_url):
|
|
||||||
from PyQt4.QtCore import QUrl
|
|
||||||
from PyQt4.QtGui import QDesktopServices
|
|
||||||
if os.path.exists(path_or_url):
|
|
||||||
path_or_url = 'file:'+path_or_url
|
|
||||||
QDesktopServices.openUrl(QUrl(path_or_url))
|
|
||||||
|
|
||||||
relpath = os.path.relpath
|
relpath = os.path.relpath
|
||||||
_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):
|
||||||
|
@ -1069,8 +1069,10 @@ class OPFCreator(MetaInformation):
|
|||||||
dc_attrs={'id':__appname__+'_id'}))
|
dc_attrs={'id':__appname__+'_id'}))
|
||||||
if getattr(self, 'pubdate', None) is not None:
|
if getattr(self, 'pubdate', None) is not None:
|
||||||
a(DC_ELEM('date', self.pubdate.isoformat()))
|
a(DC_ELEM('date', self.pubdate.isoformat()))
|
||||||
a(DC_ELEM('language', self.language if self.language else
|
lang = self.language
|
||||||
get_lang().replace('_', '-')))
|
if not lang or lang.lower() == 'und':
|
||||||
|
lang = get_lang().replace('_', '-')
|
||||||
|
a(DC_ELEM('language', lang))
|
||||||
if self.comments:
|
if self.comments:
|
||||||
a(DC_ELEM('description', self.comments))
|
a(DC_ELEM('description', self.comments))
|
||||||
if self.publisher:
|
if self.publisher:
|
||||||
|
@ -8,7 +8,7 @@ from functools import partial
|
|||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import plugins
|
from calibre.constants import plugins
|
||||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors, authors_to_string
|
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||||
|
|
||||||
pdfreflow, pdfreflow_error = plugins['pdfreflow']
|
pdfreflow, pdfreflow_error = plugins['pdfreflow']
|
||||||
|
|
||||||
@ -56,66 +56,10 @@ def get_metadata(stream, cover=True):
|
|||||||
|
|
||||||
get_quick_metadata = partial(get_metadata, cover=False)
|
get_quick_metadata = partial(get_metadata, cover=False)
|
||||||
|
|
||||||
import cStringIO
|
from calibre.utils.podofo import set_metadata as podofo_set_metadata
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from calibre.utils.pdftk import set_metadata as pdftk_set_metadata
|
|
||||||
from calibre.utils.podofo import set_metadata as podofo_set_metadata, Unavailable
|
|
||||||
|
|
||||||
def set_metadata(stream, mi):
|
def set_metadata(stream, mi):
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
try:
|
return podofo_set_metadata(stream, mi)
|
||||||
return podofo_set_metadata(stream, mi)
|
|
||||||
except Unavailable:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return pdftk_set_metadata(stream, mi)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
set_metadata_pypdf(stream, mi)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataWriter(Thread):
|
|
||||||
|
|
||||||
def __init__(self, out_pdf, buf):
|
|
||||||
self.out_pdf = out_pdf
|
|
||||||
self.buf = buf
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.daemon = True
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
self.out_pdf.write(self.buf)
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_metadata_pypdf(stream, mi):
|
|
||||||
# Use a StringIO object for the pdf because we will want to over
|
|
||||||
# write it later and if we are working on the stream directly it
|
|
||||||
# could cause some issues.
|
|
||||||
|
|
||||||
from pyPdf import PdfFileReader, PdfFileWriter
|
|
||||||
raw = cStringIO.StringIO(stream.read())
|
|
||||||
orig_pdf = PdfFileReader(raw)
|
|
||||||
title = mi.title if mi.title else orig_pdf.documentInfo.title
|
|
||||||
author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author
|
|
||||||
out_pdf = PdfFileWriter(title=title, author=author)
|
|
||||||
out_str = cStringIO.StringIO()
|
|
||||||
writer = MetadataWriter(out_pdf, out_str)
|
|
||||||
for page in orig_pdf.pages:
|
|
||||||
out_pdf.addPage(page)
|
|
||||||
writer.start()
|
|
||||||
writer.join(10) # Wait 10 secs for writing to complete
|
|
||||||
out_pdf.killed = True
|
|
||||||
writer.join()
|
|
||||||
if out_pdf.killed:
|
|
||||||
print 'Failed to set metadata: took too long'
|
|
||||||
return
|
|
||||||
|
|
||||||
stream.seek(0)
|
|
||||||
stream.truncate()
|
|
||||||
out_str.seek(0)
|
|
||||||
stream.write(out_str.read())
|
|
||||||
stream.seek(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
""" The GUI """
|
""" The GUI """
|
||||||
import os
|
import os, sys
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||||
QEvent, QTimer, pyqtSignal, QDate
|
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||||
QIcon, QApplication, QDialog, QPushButton
|
QIcon, QApplication, QDialog, QPushButton, QUrl
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre import islinux, iswindows, isosx, isfreebsd
|
from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||||
from calibre.utils.localization import set_qt_translator
|
from calibre.utils.localization import set_qt_translator
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
@ -579,6 +579,22 @@ class Application(QApplication):
|
|||||||
|
|
||||||
_store_app = None
|
_store_app = None
|
||||||
|
|
||||||
|
def open_url(qurl):
|
||||||
|
paths = os.environ.get('LD_LIBRARY_PATH',
|
||||||
|
'').split(os.pathsep)
|
||||||
|
paths = [x for x in paths if x]
|
||||||
|
if isfrozen and islinux and paths:
|
||||||
|
npaths = [x for x in paths if x != sys.frozen_path]
|
||||||
|
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
|
||||||
|
QDesktopServices.openUrl(qurl)
|
||||||
|
if isfrozen and islinux and paths:
|
||||||
|
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
||||||
|
|
||||||
|
|
||||||
|
def open_local_file(path):
|
||||||
|
url = QUrl.fromLocalFile(path)
|
||||||
|
open_url(url)
|
||||||
|
|
||||||
def is_ok_to_use_qt():
|
def is_ok_to_use_qt():
|
||||||
global gui_thread, _store_app
|
global gui_thread, _store_app
|
||||||
if (islinux or isfreebsd) and ':' not in os.environ.get('DISPLAY', ''):
|
if (islinux or isfreebsd) and ':' not in os.environ.get('DISPLAY', ''):
|
||||||
|
@ -5,17 +5,18 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import shutil, os, datetime, sys, time
|
import shutil, os, datetime, time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \
|
from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \
|
||||||
SIGNAL, QPixmap, QTimer, QDesktopServices, QUrl, QDialog
|
SIGNAL, QPixmap, QTimer, QDialog
|
||||||
|
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \
|
from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \
|
||||||
choose_dir, warning_dialog, info_dialog, question_dialog, config
|
choose_dir, warning_dialog, info_dialog, question_dialog, config, \
|
||||||
|
open_local_file
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
@ -25,7 +26,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
|||||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
||||||
fetch_scheduled_recipe, generate_catalog
|
fetch_scheduled_recipe, generate_catalog
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding, \
|
from calibre.constants import preferred_encoding, filesystem_encoding, \
|
||||||
isosx, isfrozen, islinux
|
isosx
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
@ -645,6 +646,8 @@ class EditMetadataAction(object): # {{{
|
|||||||
if x.exception is None:
|
if x.exception is None:
|
||||||
self.library_view.model().refresh_ids(
|
self.library_view.model().refresh_ids(
|
||||||
x.updated, cr)
|
x.updated, cr)
|
||||||
|
if self.cover_flow:
|
||||||
|
self.cover_flow.dataChanged()
|
||||||
if x.failures:
|
if x.failures:
|
||||||
details = ['%s: %s'%(title, reason) for title,
|
details = ['%s: %s'%(title, reason) for title,
|
||||||
reason in x.failures.values()]
|
reason in x.failures.values()]
|
||||||
@ -689,7 +692,6 @@ class EditMetadataAction(object): # {{{
|
|||||||
if rows:
|
if rows:
|
||||||
current = self.library_view.currentIndex()
|
current = self.library_view.currentIndex()
|
||||||
m = self.library_view.model()
|
m = self.library_view.model()
|
||||||
m.refresh_cover_cache(map(m.id, rows))
|
|
||||||
if self.cover_flow:
|
if self.cover_flow:
|
||||||
self.cover_flow.dataChanged()
|
self.cover_flow.dataChanged()
|
||||||
m.current_changed(current, previous)
|
m.current_changed(current, previous)
|
||||||
@ -711,6 +713,8 @@ class EditMetadataAction(object): # {{{
|
|||||||
self.library_view.model().resort(reset=False)
|
self.library_view.model().resort(reset=False)
|
||||||
self.library_view.model().research()
|
self.library_view.model().research()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
if self.cover_flow:
|
||||||
|
self.cover_flow.dataChanged()
|
||||||
|
|
||||||
# Merge books {{{
|
# Merge books {{{
|
||||||
def merge_books(self, safe_merge=False):
|
def merge_books(self, safe_merge=False):
|
||||||
@ -917,7 +921,7 @@ class SaveToDiskAction(object): # {{{
|
|||||||
_('Could not save some books') + ', ' +
|
_('Could not save some books') + ', ' +
|
||||||
_('Click the show details button to see which ones.'),
|
_('Click the show details button to see which ones.'),
|
||||||
u'\n\n'.join(failures), show=True)
|
u'\n\n'.join(failures), show=True)
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def books_saved(self, job):
|
def books_saved(self, job):
|
||||||
if job.failed:
|
if job.failed:
|
||||||
@ -1183,15 +1187,7 @@ class ViewAction(object): # {{{
|
|||||||
self.job_manager.launch_gui_app(viewer,
|
self.job_manager.launch_gui_app(viewer,
|
||||||
kwargs=dict(args=args))
|
kwargs=dict(args=args))
|
||||||
else:
|
else:
|
||||||
paths = os.environ.get('LD_LIBRARY_PATH',
|
open_local_file(name)
|
||||||
'').split(os.pathsep)
|
|
||||||
paths = [x for x in paths if x]
|
|
||||||
if isfrozen and islinux and paths:
|
|
||||||
npaths = [x for x in paths if x != sys.frozen_path]
|
|
||||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
|
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name)
|
|
||||||
if isfrozen and islinux and paths:
|
|
||||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
|
||||||
time.sleep(2) # User feedback
|
time.sleep(2) # User feedback
|
||||||
finally:
|
finally:
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
@ -1237,11 +1233,11 @@ class ViewAction(object): # {{{
|
|||||||
return
|
return
|
||||||
for row in rows:
|
for row in rows:
|
||||||
path = self.library_view.model().db.abspath(row.row())
|
path = self.library_view.model().db.abspath(row.row())
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def view_folder_for_id(self, id_):
|
def view_folder_for_id(self, id_):
|
||||||
path = self.library_view.model().db.abspath(id_, index_is_id=True)
|
path = self.library_view.model().db.abspath(id_, index_is_id=True)
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def view_book(self, triggered):
|
def view_book(self, triggered):
|
||||||
rows = self.current_view().selectionModel().selectedRows()
|
rows = self.current_view().selectionModel().selectedRows()
|
||||||
|
@ -15,7 +15,7 @@ from calibre.ebooks.metadata import MetaInformation
|
|||||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
class DuplicatesAdder(QThread):
|
class DuplicatesAdder(QThread): # {{{
|
||||||
# Add duplicate books
|
# Add duplicate books
|
||||||
def __init__(self, parent, db, duplicates, db_adder):
|
def __init__(self, parent, db, duplicates, db_adder):
|
||||||
QThread.__init__(self, parent)
|
QThread.__init__(self, parent)
|
||||||
@ -34,9 +34,9 @@ class DuplicatesAdder(QThread):
|
|||||||
self.emit(SIGNAL('added(PyQt_PyObject)'), count)
|
self.emit(SIGNAL('added(PyQt_PyObject)'), count)
|
||||||
count += 1
|
count += 1
|
||||||
self.emit(SIGNAL('adding_done()'))
|
self.emit(SIGNAL('adding_done()'))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class RecursiveFind(QThread): # {{{
|
||||||
class RecursiveFind(QThread):
|
|
||||||
|
|
||||||
def __init__(self, parent, db, root, single):
|
def __init__(self, parent, db, root, single):
|
||||||
QThread.__init__(self, parent)
|
QThread.__init__(self, parent)
|
||||||
@ -79,7 +79,9 @@ class RecursiveFind(QThread):
|
|||||||
if not self.canceled:
|
if not self.canceled:
|
||||||
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
|
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
|
||||||
|
|
||||||
class DBAdder(Thread):
|
# }}}
|
||||||
|
|
||||||
|
class DBAdder(Thread): # {{{
|
||||||
|
|
||||||
def __init__(self, db, ids, nmap):
|
def __init__(self, db, ids, nmap):
|
||||||
self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
|
self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
|
||||||
@ -219,8 +221,9 @@ class DBAdder(Thread):
|
|||||||
self.db.add_format(id, fmt, f, index_is_id=True,
|
self.db.add_format(id, fmt, f, index_is_id=True,
|
||||||
notify=False, replace=replace)
|
notify=False, replace=replace)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class Adder(QObject):
|
class Adder(QObject): # {{{
|
||||||
|
|
||||||
ADD_TIMEOUT = 600 # seconds
|
ADD_TIMEOUT = 600 # seconds
|
||||||
|
|
||||||
@ -410,6 +413,7 @@ class Adder(QObject):
|
|||||||
return getattr(getattr(self, 'db_adder', None), 'infos',
|
return getattr(getattr(self, 'db_adder', None), 'infos',
|
||||||
[])
|
[])
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
############################## END ADDER ######################################
|
############################## END ADDER ######################################
|
||||||
|
@ -9,14 +9,14 @@ import os, collections
|
|||||||
|
|
||||||
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
|
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
|
||||||
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
|
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
|
||||||
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
|
QSizePolicy, QPainter, QRect, pyqtProperty
|
||||||
|
|
||||||
from calibre import fit_image, prepare_string_for_xml
|
from calibre import fit_image, prepare_string_for_xml
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config, open_local_file
|
||||||
|
|
||||||
# render_rows(data) {{{
|
# render_rows(data) {{{
|
||||||
WEIGHTS = collections.defaultdict(lambda : 100)
|
WEIGHTS = collections.defaultdict(lambda : 100)
|
||||||
@ -294,7 +294,7 @@ class BookDetails(QWidget): # {{{
|
|||||||
id_, fmt = val.split(':')
|
id_, fmt = val.split(':')
|
||||||
self.view_specific_format.emit(int(id_), fmt)
|
self.view_specific_format.emit(int(id_), fmt)
|
||||||
elif typ == 'devpath':
|
elif typ == 'devpath':
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
|
open_local_file(val)
|
||||||
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
|
@ -31,6 +31,8 @@ from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
|||||||
config as email_config
|
config as email_config
|
||||||
from calibre.devices.apple.driver import ITUNES_ASYNC
|
from calibre.devices.apple.driver import ITUNES_ASYNC
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
||||||
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
|
from calibre.constants import DEBUG
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -304,6 +306,21 @@ class DeviceManager(Thread): # {{{
|
|||||||
|
|
||||||
def _upload_books(self, files, names, on_card=None, metadata=None):
|
def _upload_books(self, files, names, on_card=None, metadata=None):
|
||||||
'''Upload books to device: '''
|
'''Upload books to device: '''
|
||||||
|
if metadata and files and len(metadata) == len(files):
|
||||||
|
for f, mi in zip(files, metadata):
|
||||||
|
if isinstance(f, unicode):
|
||||||
|
ext = f.rpartition('.')[-1].lower()
|
||||||
|
if ext:
|
||||||
|
try:
|
||||||
|
if DEBUG:
|
||||||
|
prints('Setting metadata in:', mi.title, 'at:',
|
||||||
|
f, file=sys.__stdout__)
|
||||||
|
with open(f, 'r+b') as stream:
|
||||||
|
set_metadata(stream, mi, stream_type=ext)
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints(traceback.format_exc(), file=sys.__stdout__)
|
||||||
|
|
||||||
return self.device.upload_books(files, names, on_card,
|
return self.device.upload_books(files, names, on_card,
|
||||||
metadata=metadata, end_session=False)
|
metadata=metadata, end_session=False)
|
||||||
|
|
||||||
@ -1145,7 +1162,6 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
||||||
settings.format_map,
|
settings.format_map,
|
||||||
set_metadata=True,
|
|
||||||
specific_format=specific_format,
|
specific_format=specific_format,
|
||||||
exclude_auto=do_auto_convert)
|
exclude_auto=do_auto_convert)
|
||||||
if do_auto_convert:
|
if do_auto_convert:
|
||||||
|
@ -5,11 +5,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import textwrap, os, re
|
import textwrap, os, re
|
||||||
|
|
||||||
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt
|
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt
|
||||||
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
|
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon
|
||||||
|
|
||||||
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
||||||
from calibre.gui2 import dynamic
|
from calibre.gui2 import dynamic, open_local_file
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
|
|
||||||
@ -49,12 +49,12 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
|
|
||||||
def open_book_path(self, path):
|
def open_book_path(self, path):
|
||||||
if os.sep in unicode(path):
|
if os.sep in unicode(path):
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
else:
|
else:
|
||||||
format = unicode(path)
|
format = unicode(path)
|
||||||
path = self.view.model().db.format_abspath(self.current_row, format)
|
path = self.view.model().db.format_abspath(self.current_row, format)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
@ -123,6 +123,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
for key in info.keys():
|
for key in info.keys():
|
||||||
if key == 'id': continue
|
if key == 'id': continue
|
||||||
txt = info[key]
|
txt = info[key]
|
||||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
if key != _('Path'):
|
||||||
|
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||||
self.text.setText(u'<table>'+rows+'</table>')
|
self.text.setText(u'<table>'+rows+'</table>')
|
||||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, re, time, textwrap, copy, sys
|
import os, re, time, textwrap, copy, sys
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
QStringListModel, QAbstractItemModel, QFont, \
|
QStringListModel, QAbstractItemModel, QFont, \
|
||||||
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
||||||
QModelIndex, QAbstractTableModel, \
|
QModelIndex, QAbstractTableModel, \
|
||||||
@ -15,8 +15,9 @@ from calibre.constants import iswindows, isosx
|
|||||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
||||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
open_url, open_local_file, \
|
||||||
warning_dialog, ResizableDialog, question_dialog
|
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||||
|
warning_dialog, ResizableDialog, question_dialog
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.oeb.iterator import is_supported
|
from calibre.ebooks.oeb.iterator import is_supported
|
||||||
@ -512,7 +513,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
|
|
||||||
def open_config_dir(self):
|
def open_config_dir(self):
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(config_dir))
|
open_local_file(config_dir)
|
||||||
|
|
||||||
def create_symlinks(self):
|
def create_symlinks(self):
|
||||||
from calibre.utils.osx_symlinks import create_symlinks
|
from calibre.utils.osx_symlinks import create_symlinks
|
||||||
@ -805,7 +806,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.stop.setEnabled(False)
|
self.stop.setEnabled(False)
|
||||||
|
|
||||||
def test_server(self):
|
def test_server(self):
|
||||||
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
open_url(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
||||||
|
|
||||||
def compact(self, toggled):
|
def compact(self, toggled):
|
||||||
d = CheckIntegrity(self.db, self)
|
d = CheckIntegrity(self.db, self)
|
||||||
|
@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import time, os
|
import time, os
|
||||||
|
|
||||||
from PyQt4.Qt import SIGNAL, QUrl, QDesktopServices, QAbstractListModel, Qt, \
|
from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \
|
||||||
QVariant, QInputDialog
|
QVariant, QInputDialog
|
||||||
|
|
||||||
from calibre.web.feeds.recipes import compile_recipe
|
from calibre.web.feeds.recipes import compile_recipe
|
||||||
from calibre.web.feeds.news import AutomaticNewsRecipe
|
from calibre.web.feeds.news import AutomaticNewsRecipe
|
||||||
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
||||||
from calibre.gui2 import error_dialog, question_dialog, \
|
from calibre.gui2 import error_dialog, question_dialog, open_url, \
|
||||||
choose_files, ResizableDialog, NONE
|
choose_files, ResizableDialog, NONE
|
||||||
from calibre.gui2.widgets import PythonHighlighter
|
from calibre.gui2.widgets import PythonHighlighter
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -135,7 +135,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
|||||||
url.addQueryItem('subject', subject)
|
url.addQueryItem('subject', subject)
|
||||||
url.addQueryItem('body', body)
|
url.addQueryItem('body', body)
|
||||||
url.addQueryItem('attachment', pt.name)
|
url.addQueryItem('attachment', pt.name)
|
||||||
QDesktopServices.openUrl(url)
|
open_url(url)
|
||||||
|
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def current_changed(self, current, previous):
|
||||||
|
@ -20,7 +20,8 @@ from calibre.utils.config import tweaks, prefs
|
|||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
|
REGEXP_MATCH, CoverCache
|
||||||
from calibre.library.cli import parse_series_string
|
from calibre.library.cli import parse_series_string
|
||||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
@ -149,21 +150,22 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.build_data_convertors()
|
self.build_data_convertors()
|
||||||
self.reset()
|
self.reset()
|
||||||
self.database_changed.emit(db)
|
self.database_changed.emit(db)
|
||||||
|
if self.cover_cache is not None:
|
||||||
|
self.cover_cache.stop()
|
||||||
|
self.cover_cache = CoverCache(db)
|
||||||
|
self.cover_cache.start()
|
||||||
|
def refresh_cover(event, ids):
|
||||||
|
if event == 'cover' and self.cover_cache is not None:
|
||||||
|
self.cover_cache.refresh(ids)
|
||||||
|
db.add_listener(refresh_cover)
|
||||||
|
|
||||||
def refresh_ids(self, ids, current_row=-1):
|
def refresh_ids(self, ids, current_row=-1):
|
||||||
rows = self.db.refresh_ids(ids)
|
rows = self.db.refresh_ids(ids)
|
||||||
if rows:
|
if rows:
|
||||||
self.refresh_rows(rows, current_row=current_row)
|
self.refresh_rows(rows, current_row=current_row)
|
||||||
|
|
||||||
def refresh_cover_cache(self, ids):
|
|
||||||
if self.cover_cache:
|
|
||||||
self.cover_cache.refresh(ids)
|
|
||||||
|
|
||||||
def refresh_rows(self, rows, current_row=-1):
|
def refresh_rows(self, rows, current_row=-1):
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if self.cover_cache:
|
|
||||||
id = self.db.id(row)
|
|
||||||
self.cover_cache.refresh([id])
|
|
||||||
if row == current_row:
|
if row == current_row:
|
||||||
self.new_bookdisplay_data.emit(
|
self.new_bookdisplay_data.emit(
|
||||||
self.get_book_display_info(row))
|
self.get_book_display_info(row))
|
||||||
@ -326,7 +328,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def set_cache(self, idx):
|
def set_cache(self, idx):
|
||||||
l, r = 0, self.count()-1
|
l, r = 0, self.count()-1
|
||||||
if self.cover_cache:
|
if self.cover_cache is not None:
|
||||||
l = max(l, idx-self.buffer_size)
|
l = max(l, idx-self.buffer_size)
|
||||||
r = min(r, idx+self.buffer_size)
|
r = min(r, idx+self.buffer_size)
|
||||||
k = min(r-idx, idx-l)
|
k = min(r-idx, idx-l)
|
||||||
@ -494,11 +496,9 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
id = self.db.id(row_number)
|
id = self.db.id(row_number)
|
||||||
if self.cover_cache:
|
if self.cover_cache is not None:
|
||||||
img = self.cover_cache.cover(id)
|
img = self.cover_cache.cover(id)
|
||||||
if img:
|
if not img.isNull():
|
||||||
if img.isNull():
|
|
||||||
img = self.default_image
|
|
||||||
return img
|
return img
|
||||||
if not data:
|
if not data:
|
||||||
data = self.db.cover(row_number)
|
data = self.db.cover(row_number)
|
||||||
|
@ -12,9 +12,9 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import collections, os, sys, textwrap, time
|
import collections, os, sys, textwrap, time
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
|
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \
|
||||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||||
QDialog, QDesktopServices, \
|
QDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QHelpEvent
|
QMessageBox, QHelpEvent
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ from calibre.constants import __version__, __appname__, isosx
|
|||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.gui2 import error_dialog, GetMetadata, \
|
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
||||||
gprefs, max_available_height, config, info_dialog
|
gprefs, max_available_height, config, info_dialog
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
@ -38,7 +38,6 @@ from calibre.gui2.dialogs.config import ConfigDialog
|
|||||||
|
|
||||||
from calibre.gui2.dialogs.book_info import BookInfo
|
from calibre.gui2.dialogs.book_info import BookInfo
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.caches import CoverCache
|
|
||||||
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
@ -138,6 +137,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.restriction_in_effect = False
|
self.restriction_in_effect = False
|
||||||
|
|
||||||
self.progress_indicator = ProgressIndicator(self)
|
self.progress_indicator = ProgressIndicator(self)
|
||||||
|
self.progress_indicator.pos = (0, 20)
|
||||||
self.verbose = opts.verbose
|
self.verbose = opts.verbose
|
||||||
self.get_metadata = GetMetadata()
|
self.get_metadata = GetMetadata()
|
||||||
self.upload_memory = {}
|
self.upload_memory = {}
|
||||||
@ -230,9 +230,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
|
|
||||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
self.cover_cache = CoverCache(self.library_path)
|
|
||||||
self.cover_cache.start()
|
|
||||||
self.library_view.model().cover_cache = self.cover_cache
|
|
||||||
self.library_view.model().count_changed_signal.connect \
|
self.library_view.model().count_changed_signal.connect \
|
||||||
(self.location_view.count_changed)
|
(self.location_view.count_changed)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
@ -575,7 +572,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
pt = PersistentTemporaryFile('_donate.htm')
|
pt = PersistentTemporaryFile('_donate.htm')
|
||||||
pt.write(HTML.encode('utf-8'))
|
pt.write(HTML.encode('utf-8'))
|
||||||
pt.close()
|
pt.close()
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
|
open_local_file(pt.name)
|
||||||
|
|
||||||
|
|
||||||
def confirm_quit(self):
|
def confirm_quit(self):
|
||||||
@ -606,9 +603,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
while self.spare_servers:
|
while self.spare_servers:
|
||||||
self.spare_servers.pop().close()
|
self.spare_servers.pop().close()
|
||||||
self.device_manager.keep_going = False
|
self.device_manager.keep_going = False
|
||||||
self.cover_cache.stop()
|
cc = self.library_view.model().cover_cache
|
||||||
|
if cc is not None:
|
||||||
|
cc.stop()
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
self.cover_cache.terminate()
|
|
||||||
self.emailer.stop()
|
self.emailer.stop()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
|
from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre.constants import __appname__, __version__, iswindows, isosx
|
from calibre.constants import __appname__, __version__, iswindows, isosx
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2 import config, dynamic, question_dialog
|
from calibre.gui2 import config, dynamic, question_dialog, open_url
|
||||||
|
|
||||||
URL = 'http://status.calibre-ebook.com/latest'
|
URL = 'http://status.calibre-ebook.com/latest'
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ class UpdateMixin(object):
|
|||||||
'ge?')%(__appname__, version)):
|
'ge?')%(__appname__, version)):
|
||||||
url = 'http://calibre-ebook.com/download_'+\
|
url = 'http://calibre-ebook.com/download_'+\
|
||||||
('windows' if iswindows else 'osx' if isosx else 'linux')
|
('windows' if iswindows else 'osx' if isosx else 'linux')
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
open_url(QUrl(url))
|
||||||
dynamic.set('update to version %s'%version, False)
|
dynamic.set('update to version %s'%version, False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from functools import partial
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, QByteArray, \
|
from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, QByteArray, \
|
||||||
QDesktopServices, QDoubleSpinBox, QLabel, QTextBrowser, \
|
QDoubleSpinBox, QLabel, QTextBrowser, \
|
||||||
QPainter, QBrush, QColor, QStandardItemModel, QPalette, \
|
QPainter, QBrush, QColor, QStandardItemModel, QPalette, \
|
||||||
QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \
|
QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \
|
||||||
QToolButton, QMenu, QInputDialog, QAction, QKeySequence
|
QToolButton, QMenu, QInputDialog, QAction, QKeySequence
|
||||||
@ -17,7 +17,7 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
|
|||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
||||||
info_dialog, error_dialog
|
info_dialog, error_dialog, open_url
|
||||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre.constants import islinux, isfreebsd
|
from calibre.constants import islinux, isfreebsd
|
||||||
@ -472,7 +472,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
elif frag:
|
elif frag:
|
||||||
self.view.scroll_to(frag)
|
self.view.scroll_to(frag)
|
||||||
else:
|
else:
|
||||||
QDesktopServices.openUrl(url)
|
open_url(url)
|
||||||
|
|
||||||
def load_started(self):
|
def load_started(self):
|
||||||
self.open_progress_indicator(_('Loading flow...'))
|
self.open_progress_indicator(_('Loading flow...'))
|
||||||
|
@ -38,12 +38,16 @@ class ProgressIndicator(QWidget):
|
|||||||
self.status.setWordWrap(True)
|
self.status.setWordWrap(True)
|
||||||
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
|
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
|
self.pos = None
|
||||||
|
|
||||||
def start(self, msg=''):
|
def start(self, msg=''):
|
||||||
view = self.parent()
|
view = self.parent()
|
||||||
pwidth, pheight = view.size().width(), view.size().height()
|
pwidth, pheight = view.size().width(), view.size().height()
|
||||||
self.resize(pwidth, min(pheight, 250))
|
self.resize(pwidth, min(pheight, 250))
|
||||||
self.move(0, (pheight-self.size().height())/2.)
|
if self.pos is None:
|
||||||
|
self.move(0, (pheight-self.size().height())/2.)
|
||||||
|
else:
|
||||||
|
self.move(self.pos[0], self.pos[1])
|
||||||
self.pi.resize(self.pi.sizeHint())
|
self.pi.resize(self.pi.sizeHint())
|
||||||
self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0)
|
self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0)
|
||||||
self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10)
|
self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10)
|
||||||
|
@ -6,11 +6,13 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import collections, glob, os, re, itertools, functools
|
import re, itertools, functools
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from threading import Thread, RLock
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
|
||||||
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
|
from PyQt4.Qt import QImage, Qt
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
@ -19,120 +21,73 @@ from calibre.utils.pyparsing import ParseException
|
|||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(Thread):
|
||||||
|
|
||||||
def __init__(self, library_path, parent=None):
|
def __init__(self, db):
|
||||||
QThread.__init__(self, parent)
|
Thread.__init__(self)
|
||||||
self.library_path = library_path
|
self.daemon = True
|
||||||
self.id_map = None
|
self.db = db
|
||||||
self.id_map_lock = QReadWriteLock()
|
self.load_queue = Queue()
|
||||||
self.load_queue = collections.deque()
|
|
||||||
self.load_queue_lock = QReadWriteLock(QReadWriteLock.Recursive)
|
|
||||||
self.cache = {}
|
|
||||||
self.cache_lock = QReadWriteLock()
|
|
||||||
self.id_map_stale = True
|
|
||||||
self.keep_running = True
|
self.keep_running = True
|
||||||
|
self.cache = {}
|
||||||
def build_id_map(self):
|
self.lock = RLock()
|
||||||
self.id_map_lock.lockForWrite()
|
self.null_image = QImage()
|
||||||
self.id_map = {}
|
|
||||||
for f in glob.glob(os.path.join(self.library_path, '*', '* (*)', 'cover.jpg')):
|
|
||||||
c = os.path.basename(os.path.dirname(f))
|
|
||||||
try:
|
|
||||||
id = int(re.search(r'\((\d+)\)', c[c.rindex('('):]).group(1))
|
|
||||||
self.id_map[id] = f
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
self.id_map_lock.unlock()
|
|
||||||
self.id_map_stale = False
|
|
||||||
|
|
||||||
|
|
||||||
def set_cache(self, ids):
|
|
||||||
self.cache_lock.lockForWrite()
|
|
||||||
already_loaded = set([])
|
|
||||||
for id in self.cache.keys():
|
|
||||||
if id in ids:
|
|
||||||
already_loaded.add(id)
|
|
||||||
else:
|
|
||||||
self.cache.pop(id)
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
ids = [i for i in ids if i not in already_loaded]
|
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
self.load_queue = collections.deque(ids)
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while self.keep_running:
|
|
||||||
if self.id_map is None or self.id_map_stale:
|
|
||||||
self.build_id_map()
|
|
||||||
while True: # Load images from the load queue
|
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
try:
|
|
||||||
id = self.load_queue.popleft()
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
self.cache_lock.lockForRead()
|
|
||||||
need = True
|
|
||||||
if id in self.cache.keys():
|
|
||||||
need = False
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
if not need:
|
|
||||||
continue
|
|
||||||
path = None
|
|
||||||
self.id_map_lock.lockForRead()
|
|
||||||
if id in self.id_map.keys():
|
|
||||||
path = self.id_map[id]
|
|
||||||
else:
|
|
||||||
self.id_map_stale = True
|
|
||||||
self.id_map_lock.unlock()
|
|
||||||
if path and os.access(path, os.R_OK):
|
|
||||||
try:
|
|
||||||
img = QImage()
|
|
||||||
data = open(path, 'rb').read()
|
|
||||||
img.loadFromData(data)
|
|
||||||
if img.isNull():
|
|
||||||
continue
|
|
||||||
scaled, nwidth, nheight = fit_image(img.width(),
|
|
||||||
img.height(), 600, 800)
|
|
||||||
if scaled:
|
|
||||||
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
|
||||||
Qt.SmoothTransformation)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
self.cache_lock.lockForWrite()
|
|
||||||
self.cache[id] = img
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
|
|
||||||
self.sleep(1)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.keep_running = False
|
self.keep_running = False
|
||||||
|
|
||||||
def cover(self, id):
|
def _image_for_id(self, id_):
|
||||||
val = None
|
img = self.db.cover(id_, index_is_id=True, as_image=True)
|
||||||
if self.cache_lock.tryLockForRead(50):
|
if img is None:
|
||||||
val = self.cache.get(id, None)
|
img = QImage()
|
||||||
self.cache_lock.unlock()
|
if not img.isNull():
|
||||||
return val
|
scaled, nwidth, nheight = fit_image(img.width(),
|
||||||
|
img.height(), 600, 800)
|
||||||
|
if scaled:
|
||||||
|
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.keep_running:
|
||||||
|
try:
|
||||||
|
id_ = self.load_queue.get(True, 1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
img = self._image_for_id(id_)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
with self.lock:
|
||||||
|
self.cache[id_] = img
|
||||||
|
|
||||||
|
def set_cache(self, ids):
|
||||||
|
with self.lock:
|
||||||
|
already_loaded = set([])
|
||||||
|
for id in self.cache.keys():
|
||||||
|
if id in ids:
|
||||||
|
already_loaded.add(id)
|
||||||
|
else:
|
||||||
|
self.cache.pop(id)
|
||||||
|
for id_ in set(ids) - already_loaded:
|
||||||
|
self.load_queue.put(id_)
|
||||||
|
|
||||||
|
def cover(self, id_):
|
||||||
|
with self.lock:
|
||||||
|
return self.cache.get(id_, self.null_image)
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
self.cache_lock.lockForWrite()
|
with self.lock:
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.cache_lock.unlock()
|
|
||||||
|
|
||||||
def refresh(self, ids):
|
def refresh(self, ids):
|
||||||
self.cache_lock.lockForWrite()
|
with self.lock:
|
||||||
for id in ids:
|
for id_ in ids:
|
||||||
self.cache.pop(id, None)
|
self.cache.pop(id_, None)
|
||||||
self.cache_lock.unlock()
|
self.load_queue.put(id_)
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
for id in ids:
|
|
||||||
self.load_queue.appendleft(id)
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
### Global utility function for get_match here and in gui2/library.py
|
### Global utility function for get_match here and in gui2/library.py
|
||||||
CONTAINS_MATCH = 0
|
CONTAINS_MATCH = 0
|
||||||
@ -341,8 +296,15 @@ class ResultCache(SearchQueryParser):
|
|||||||
cast = lambda x : float (x)
|
cast = lambda x : float (x)
|
||||||
adjust = lambda x: x
|
adjust = lambda x: x
|
||||||
|
|
||||||
|
if len(query) > 1:
|
||||||
|
mult = query[-1:].lower()
|
||||||
|
mult = {'k':1024.,'m': 1024.**2, 'g': 1024.**3}.get(mult, 1.0)
|
||||||
|
if mult != 1.0:
|
||||||
|
query = query[:-1]
|
||||||
|
else:
|
||||||
|
mult = 1.0
|
||||||
try:
|
try:
|
||||||
q = cast(query)
|
q = cast(query) * mult
|
||||||
except:
|
except:
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
@ -8,9 +8,14 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString, \
|
||||||
|
CData, Comment, Declaration, ProcessingInstruction
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
|
|
||||||
|
# Hackish - ignoring sentences ending or beginning in numbers to avoid
|
||||||
|
# confusion with decimal points.
|
||||||
|
lost_cr_pat = re.compile('([a-z])([\.\?!])([A-Z])')
|
||||||
|
|
||||||
def comments_to_html(comments):
|
def comments_to_html(comments):
|
||||||
'''
|
'''
|
||||||
Convert random comment text to normalized, xml-legal block of <p>s
|
Convert random comment text to normalized, xml-legal block of <p>s
|
||||||
@ -41,36 +46,25 @@ def comments_to_html(comments):
|
|||||||
|
|
||||||
if '<' not in comments:
|
if '<' not in comments:
|
||||||
comments = prepare_string_for_xml(comments)
|
comments = prepare_string_for_xml(comments)
|
||||||
comments = comments.replace(u'\n', u'<br />')
|
parts = [u'<p class="description">%s</p>'%x.replace(u'\n', u'<br />')
|
||||||
return u'<p>%s</p>'%comments
|
for x in comments.split('\n\n')]
|
||||||
|
return '\n'.join(parts)
|
||||||
# Hackish - ignoring sentences ending or beginning in numbers to avoid
|
|
||||||
# confusion with decimal points.
|
|
||||||
|
|
||||||
# Explode lost CRs to \n\n
|
# Explode lost CRs to \n\n
|
||||||
for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])', comments):
|
for lost_cr in lost_cr_pat.finditer(comments):
|
||||||
comments = comments.replace(lost_cr.group(),
|
comments = comments.replace(lost_cr.group(),
|
||||||
'%s%s\n\n%s' % (lost_cr.group(1),
|
'%s%s\n\n%s' % (lost_cr.group(1),
|
||||||
lost_cr.group(2),
|
lost_cr.group(2),
|
||||||
lost_cr.group(3)))
|
lost_cr.group(3)))
|
||||||
|
|
||||||
|
comments = comments.replace(u'\r', u'')
|
||||||
# Convert \n\n to <p>s
|
# Convert \n\n to <p>s
|
||||||
if re.search('\n\n', comments):
|
comments = comments.replace(u'\n\n', u'<p>')
|
||||||
soup = BeautifulSoup()
|
|
||||||
split_ps = comments.split(u'\n\n')
|
|
||||||
tsc = 0
|
|
||||||
for p in split_ps:
|
|
||||||
pTag = Tag(soup,'p')
|
|
||||||
pTag.insert(0,p)
|
|
||||||
soup.insert(tsc,pTag)
|
|
||||||
tsc += 1
|
|
||||||
comments = soup.renderContents(None)
|
|
||||||
|
|
||||||
# Convert solo returns to <br />
|
# Convert solo returns to <br />
|
||||||
comments = re.sub('[\r\n]','<br />', comments)
|
comments = comments.replace(u'\n', '<br />')
|
||||||
|
|
||||||
# Convert two hyphens to emdash
|
# Convert two hyphens to emdash
|
||||||
comments = re.sub('--', '—', comments)
|
comments = comments.replace('--', '—')
|
||||||
|
|
||||||
soup = BeautifulSoup(comments)
|
soup = BeautifulSoup(comments)
|
||||||
result = BeautifulSoup()
|
result = BeautifulSoup()
|
||||||
rtc = 0
|
rtc = 0
|
||||||
@ -85,35 +79,52 @@ def comments_to_html(comments):
|
|||||||
ptc = 0
|
ptc = 0
|
||||||
pTag.insert(ptc,prepare_string_for_xml(token))
|
pTag.insert(ptc,prepare_string_for_xml(token))
|
||||||
ptc += 1
|
ptc += 1
|
||||||
|
elif type(token) in (CData, Comment, Declaration,
|
||||||
elif token.name in ['br','b','i','em']:
|
ProcessingInstruction):
|
||||||
|
continue
|
||||||
|
elif token.name in ['br', 'b', 'i', 'em', 'strong', 'span', 'font', 'a',
|
||||||
|
'hr']:
|
||||||
if not open_pTag:
|
if not open_pTag:
|
||||||
pTag = Tag(result,'p')
|
pTag = Tag(result,'p')
|
||||||
open_pTag = True
|
open_pTag = True
|
||||||
ptc = 0
|
ptc = 0
|
||||||
pTag.insert(ptc, token)
|
pTag.insert(ptc, token)
|
||||||
ptc += 1
|
ptc += 1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if open_pTag:
|
if open_pTag:
|
||||||
result.insert(rtc, pTag)
|
result.insert(rtc, pTag)
|
||||||
rtc += 1
|
rtc += 1
|
||||||
open_pTag = False
|
open_pTag = False
|
||||||
ptc = 0
|
ptc = 0
|
||||||
# Clean up NavigableStrings for xml
|
|
||||||
sub_tokens = list(token.contents)
|
|
||||||
for sub_token in sub_tokens:
|
|
||||||
if type(sub_token) is NavigableString:
|
|
||||||
sub_token.replaceWith(prepare_string_for_xml(sub_token))
|
|
||||||
result.insert(rtc, token)
|
result.insert(rtc, token)
|
||||||
rtc += 1
|
rtc += 1
|
||||||
|
|
||||||
if open_pTag:
|
if open_pTag:
|
||||||
result.insert(rtc, pTag)
|
result.insert(rtc, pTag)
|
||||||
|
|
||||||
paras = result.findAll('p')
|
for p in result.findAll('p'):
|
||||||
for p in paras:
|
|
||||||
p['class'] = 'description'
|
p['class'] = 'description'
|
||||||
|
|
||||||
|
for t in result.findAll(text=True):
|
||||||
|
t.replaceWith(prepare_string_for_xml(unicode(t)))
|
||||||
|
|
||||||
return result.renderContents(encoding=None)
|
return result.renderContents(encoding=None)
|
||||||
|
|
||||||
|
def test():
|
||||||
|
for pat, val in [
|
||||||
|
('lineone\n\nlinetwo',
|
||||||
|
'<p class="description">lineone</p>\n<p class="description">linetwo</p>'),
|
||||||
|
('a <b>b&c</b>\nf', '<p class="description">a <b>b&c;</b><br />f</p>'),
|
||||||
|
('a <?xml asd> b\n\ncd', '<p class="description">a b</p><p class="description">cd</p>'),
|
||||||
|
]:
|
||||||
|
print
|
||||||
|
print 'Testing: %r'%pat
|
||||||
|
cval = comments_to_html(pat)
|
||||||
|
print 'Value: %r'%cval
|
||||||
|
if comments_to_html(pat) != val:
|
||||||
|
print 'FAILED'
|
||||||
|
break
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
The database used to store ebook metadata
|
The database used to store ebook metadata
|
||||||
'''
|
'''
|
||||||
import os, sys, shutil, cStringIO, glob,functools, traceback
|
import os, sys, shutil, cStringIO, glob, time, functools, traceback
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import floor
|
from math import floor
|
||||||
|
|
||||||
@ -440,12 +440,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if os.access(path, os.R_OK):
|
if os.access(path, os.R_OK):
|
||||||
if as_path:
|
if as_path:
|
||||||
return path
|
return path
|
||||||
f = open(path, 'rb')
|
try:
|
||||||
|
f = open(path, 'rb')
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
f = open(path, 'rb')
|
||||||
if as_image:
|
if as_image:
|
||||||
img = QImage()
|
img = QImage()
|
||||||
img.loadFromData(f.read())
|
img.loadFromData(f.read())
|
||||||
|
f.close()
|
||||||
return img
|
return img
|
||||||
return f if as_file else f.read()
|
ans = f if as_file else f.read()
|
||||||
|
if ans is not f:
|
||||||
|
f.close()
|
||||||
|
return ans
|
||||||
|
|
||||||
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
||||||
'''
|
'''
|
||||||
@ -492,12 +500,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
return os.access(path, os.R_OK)
|
return os.access(path, os.R_OK)
|
||||||
|
|
||||||
def remove_cover(self, id):
|
def remove_cover(self, id, notify=True):
|
||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.remove(path)
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
os.remove(path)
|
||||||
|
if notify:
|
||||||
|
self.notify('cover', [id])
|
||||||
|
|
||||||
def set_cover(self, id, data):
|
def set_cover(self, id, data, notify=True):
|
||||||
'''
|
'''
|
||||||
Set the cover for this book.
|
Set the cover for this book.
|
||||||
|
|
||||||
@ -509,7 +523,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
else:
|
else:
|
||||||
if callable(getattr(data, 'read', None)):
|
if callable(getattr(data, 'read', None)):
|
||||||
data = data.read()
|
data = data.read()
|
||||||
save_cover_data_to(data, path)
|
try:
|
||||||
|
save_cover_data_to(data, path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
save_cover_data_to(data, path)
|
||||||
|
if notify:
|
||||||
|
self.notify('cover', [id])
|
||||||
|
|
||||||
def book_on_device(self, id):
|
def book_on_device(self, id):
|
||||||
if callable(self.book_on_device_func):
|
if callable(self.book_on_device_func):
|
||||||
|
@ -253,7 +253,7 @@ class FieldMetadata(dict):
|
|||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':None,
|
||||||
'search_terms':[],
|
'search_terms':['size'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False}),
|
'is_category':False}),
|
||||||
('timestamp', {'table':None,
|
('timestamp', {'table':None,
|
||||||
|
@ -199,31 +199,59 @@ Searches are by default 'contains'. An item matches if the search string appears
|
|||||||
Two other kinds of searches are available: equality search and search using regular expressions.
|
Two other kinds of searches are available: equality search and search using regular expressions.
|
||||||
|
|
||||||
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
||||||
``tag:"=science"`` will match "science", but not "science fiction". Regular expression searches are
|
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
|
||||||
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
||||||
be used. Regular expression searches are contains searches unless the expression contains anchors.
|
be used. Regular expression searches are contains searches unless the expression contains anchors.
|
||||||
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
||||||
|
|
||||||
|
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
|
||||||
|
for the tag ``Science Fiction``, you would need to search for ``tag:"=science fiction"``. If you search for
|
||||||
|
``tag:=science fiction``, you will find all books with the tag 'science' and containing the word 'fiction' in any
|
||||||
|
metadata.
|
||||||
|
|
||||||
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
|
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
|
||||||
clicking the button |sbi|.
|
clicking the button |sbi|.
|
||||||
|
|
||||||
Available fields for searching are: ``tag, title, author, publisher, series, rating cover, comments, format,
|
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format,
|
||||||
isbn, date, pubdate, search``.
|
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field
|
||||||
|
becomes available. To find the search name for a custom column, hover your mouse over the column header.
|
||||||
|
|
||||||
The syntax for searching for dates and publication dates is::
|
The syntax for searching for dates is::
|
||||||
|
|
||||||
pubdate:>2000-1 Will find all books published after Jan, 2000
|
pubdate:>2000-1 Will find all books published after Jan, 2000
|
||||||
date:<=2000-1-3 Will find all books added to calibre beforre 3 Jan, 2000
|
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
|
||||||
pubdate:=2009 Will find all books published in 2009
|
pubdate:=2009 Will find all books published in 2009
|
||||||
|
|
||||||
|
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
|
||||||
|
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009.
|
||||||
|
|
||||||
|
Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The
|
||||||
|
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
|
||||||
|
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
|
||||||
|
|
||||||
|
You can search for books that have a format of a certain size like this::
|
||||||
|
|
||||||
|
size:>1.1M Will find books with a format larger than 1.1MB
|
||||||
|
size:<=1K Will find books with a format smaller than 1KB
|
||||||
|
|
||||||
|
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or
|
||||||
|
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are
|
||||||
|
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher.
|
||||||
|
|
||||||
The special field ``search`` is used for saved searches. So if you save a search with the name
|
The special field ``search`` is used for saved searches. So if you save a search with the name
|
||||||
"My spouse's books" you can enter ``search:"My spouses' books"`` in the search bar to reuse the saved
|
"My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved
|
||||||
search. More about saving searches, below.
|
search. More about saving searches, below.
|
||||||
|
|
||||||
You can search for the absence or presnce of a filed using the specia "true" and "false" values. For example::
|
You can search for the absence or presence of a field using the special "true" and "false" values. For example::
|
||||||
|
|
||||||
cover:false Will give you all books without a cover
|
cover:false will give you all books without a cover
|
||||||
series:true Will give you all books that belong to a series
|
series:true will give you all books that belong to a series
|
||||||
|
comments:false will give you all books with an empty comment
|
||||||
|
|
||||||
|
Yes/no custom columns are searchable. Searching for ``false``, ``empty``, or ``blank`` will find all books
|
||||||
|
with undefined values in the column. Searching for ``true`` will find all books that do not have undefined
|
||||||
|
values in the column. Searching for ``yes`` or ``checked`` will find all books with ``Yes`` in the column.
|
||||||
|
Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the column.
|
||||||
|
|
||||||
.. |sbi| image:: images/search_button.png
|
.. |sbi| image:: images/search_button.png
|
||||||
:align: middle
|
:align: middle
|
||||||
|
@ -14,6 +14,7 @@ from calibre.ebooks.metadata import MetaInformation, string_to_authors, \
|
|||||||
from calibre.utils.ipc.job import ParallelJob
|
from calibre.utils.ipc.job import ParallelJob
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
podofo, podofo_err = plugins['podofo']
|
podofo, podofo_err = plugins['podofo']
|
||||||
|
|
||||||
@ -117,12 +118,18 @@ def set_metadata(stream, mi):
|
|||||||
|
|
||||||
job.update()
|
job.update()
|
||||||
server.close()
|
server.close()
|
||||||
if job.result is not None:
|
if job.failed:
|
||||||
|
prints(job.details)
|
||||||
|
elif job.result is not None:
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
stream.truncate()
|
stream.truncate()
|
||||||
stream.write(job.result)
|
stream.write(job.result)
|
||||||
stream.flush()
|
stream.flush()
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
|
try:
|
||||||
|
os.remove(pt.name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,15 +6,10 @@
|
|||||||
#include <podofo.h>
|
#include <podofo.h>
|
||||||
using namespace PoDoFo;
|
using namespace PoDoFo;
|
||||||
|
|
||||||
class podofo_pdfmem_wrapper : public PdfMemDocument {
|
|
||||||
public:
|
|
||||||
inline void set_info(PdfInfo *i) { this->SetInfo(i); }
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
/* Type-specific fields go here. */
|
/* Type-specific fields go here. */
|
||||||
podofo_pdfmem_wrapper *doc;
|
PdfMemDocument *doc;
|
||||||
|
|
||||||
} podofo_PDFDoc;
|
} podofo_PDFDoc;
|
||||||
|
|
||||||
@ -33,7 +28,7 @@ podofo_PDFDoc_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
|||||||
|
|
||||||
self = (podofo_PDFDoc *)type->tp_alloc(type, 0);
|
self = (podofo_PDFDoc *)type->tp_alloc(type, 0);
|
||||||
if (self != NULL) {
|
if (self != NULL) {
|
||||||
self->doc = new podofo_pdfmem_wrapper();
|
self->doc = new PdfMemDocument();
|
||||||
if (self->doc == NULL) { Py_DECREF(self); return NULL; }
|
if (self->doc == NULL) { Py_DECREF(self); return NULL; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,8 +55,7 @@ podofo_PDFDoc_load(podofo_PDFDoc *self, PyObject *args, PyObject *kwargs) {
|
|||||||
} else return NULL;
|
} else return NULL;
|
||||||
|
|
||||||
|
|
||||||
Py_INCREF(Py_None);
|
Py_RETURN_NONE;
|
||||||
return Py_None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
@ -78,8 +72,7 @@ podofo_PDFDoc_open(podofo_PDFDoc *self, PyObject *args, PyObject *kwargs) {
|
|||||||
} else return NULL;
|
} else return NULL;
|
||||||
|
|
||||||
|
|
||||||
Py_INCREF(Py_None);
|
Py_RETURN_NONE;
|
||||||
return Py_None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
@ -171,6 +164,19 @@ podofo_convert_pystring(PyObject *py) {
|
|||||||
return ans;
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PdfString *
|
||||||
|
podofo_convert_pystring_single_byte(PyObject *py) {
|
||||||
|
Py_UNICODE* u = PyUnicode_AS_UNICODE(py);
|
||||||
|
PyObject *s = PyUnicode_Encode(u, PyUnicode_GET_SIZE(py), "cp1252", "replace");
|
||||||
|
if (s == NULL) { PyErr_NoMemory(); return NULL; }
|
||||||
|
PdfString *ans = new PdfString(PyString_AS_STRING(s));
|
||||||
|
Py_DECREF(s);
|
||||||
|
if (ans == NULL) PyErr_NoMemory();
|
||||||
|
return ans;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
podofo_PDFDoc_getter(podofo_PDFDoc *self, int field)
|
podofo_PDFDoc_getter(podofo_PDFDoc *self, int field)
|
||||||
{
|
{
|
||||||
@ -219,7 +225,10 @@ podofo_PDFDoc_setter(podofo_PDFDoc *self, PyObject *val, int field) {
|
|||||||
PyErr_SetString(PyExc_Exception, "You must first load a PDF Document");
|
PyErr_SetString(PyExc_Exception, "You must first load a PDF Document");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
PdfString *s = podofo_convert_pystring(val);
|
PdfString *s = NULL;
|
||||||
|
|
||||||
|
if (self->doc->GetEncrypted()) s = podofo_convert_pystring_single_byte(val);
|
||||||
|
else s = podofo_convert_pystring(val);
|
||||||
if (s == NULL) return -1;
|
if (s == NULL) return -1;
|
||||||
|
|
||||||
|
|
||||||
@ -241,9 +250,6 @@ podofo_PDFDoc_setter(podofo_PDFDoc *self, PyObject *val, int field) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self->doc->set_info(info);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
src/calibre/utils/podofo/test.cpp
Normal file
26
src/calibre/utils/podofo/test.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#define USING_SHARED_PODOFO
|
||||||
|
#include <podofo.h>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace PoDoFo;
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) return 1;
|
||||||
|
char *fname = argv[1];
|
||||||
|
|
||||||
|
PdfMemDocument doc(fname);
|
||||||
|
PdfInfo* info = doc.GetInfo();
|
||||||
|
cout << endl;
|
||||||
|
cout << "is encrypted: " << doc.GetEncrypted() << endl;
|
||||||
|
PdfString old_title = info->GetTitle();
|
||||||
|
cout << "is hex: " << old_title.IsHex() << endl;
|
||||||
|
PdfString new_title(reinterpret_cast<const pdf_utf16be*>("\0z\0z\0z"), 3);
|
||||||
|
cout << "is new unicode: " << new_title.IsUnicode() << endl;
|
||||||
|
info->SetTitle(new_title);
|
||||||
|
|
||||||
|
doc.Write("/t/x.pdf");
|
||||||
|
cout << "Output written to: " << "/t/x.pdf" << endl;
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user