%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/auto_add.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import os
import shutil
import tempfile
import time
from qt.core import (
QApplication, QCursor, QFileSystemWatcher, QObject, Qt, QTimer, pyqtSignal
)
from threading import Event, Thread
from calibre import prints
from calibre.db.adding import compile_rule, filter_filename
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2 import gprefs
from calibre.gui2.dialogs.duplicates import DuplicatesQuestion
from calibre.utils.tdir_in_cache import tdir_in_cache
AUTO_ADDED = frozenset(BOOK_EXTENSIONS) - {'pdr', 'mbp', 'tan'}
class AllAllowed:
def __init__(self):
self.disallowed = frozenset(gprefs['blocked_auto_formats'])
def __contains__(self, x):
return x not in self.disallowed
def allowed_formats():
' Return an object that can be used to test if a format (lowercase) is allowed for auto-adding '
if gprefs['auto_add_everything']:
allowed = AllAllowed()
else:
allowed = AUTO_ADDED - frozenset(gprefs['blocked_auto_formats'])
return allowed
class Worker(Thread):
def __init__(self, path, callback):
Thread.__init__(self)
self.daemon = True
self.keep_running = True
self.wake_up = Event()
self.path, self.callback = path, callback
self.staging = set()
self.allowed = allowed_formats()
self.read_rules()
def read_rules(self):
try:
self.compiled_rules = tuple(map(compile_rule, gprefs.get('add_filter_rules', ())))
except Exception:
self.compiled_rules = ()
import traceback
traceback.print_exc()
def is_filename_allowed(self, filename):
allowed = filter_filename(self.compiled_rules, filename)
if allowed is None:
ext = os.path.splitext(filename)[1][1:].lower()
allowed = ext in self.allowed
return allowed
def run(self):
self.tdir = tdir_in_cache('aa')
try:
while self.keep_running:
self.wake_up.wait()
self.wake_up.clear()
if not self.keep_running:
break
try:
self.auto_add()
except:
import traceback
traceback.print_exc()
finally:
shutil.rmtree(self.tdir, ignore_errors=True)
def auto_add(self):
from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.utils.ipc.simple_worker import WorkerError, fork_job
files = [x for x in os.listdir(self.path) if
# Must not be in the process of being added to the db
x not in self.staging and
# Firefox creates 0 byte placeholder files when downloading
os.stat(os.path.join(self.path, x)).st_size > 0 and
# Must be a file
os.path.isfile(os.path.join(self.path, x)) and
# Must have read and write permissions
os.access(os.path.join(self.path, x), os.R_OK|os.W_OK) and
# Must be a known ebook file type
self.is_filename_allowed(x)
]
data = []
# Give any in progress copies time to complete
time.sleep(2)
def safe_mtime(x):
try:
return os.path.getmtime(os.path.join(self.path, x))
except OSError:
return time.time()
for fname in sorted(files, key=safe_mtime):
f = os.path.join(self.path, fname)
# Try opening the file for reading, if the OS prevents us, then at
# least on windows, it means the file is open in another
# application for writing. We will get notified by
# QFileSystemWatcher when writing is completed, so ignore for now.
try:
open(f, 'rb').close()
except:
continue
tdir = tempfile.mkdtemp(dir=self.tdir)
try:
fork_job('calibre.ebooks.metadata.meta',
'forked_read_metadata', (f, tdir), no_output=True)
except WorkerError as e:
prints('Failed to read metadata from:', fname)
prints(e.orig_tb)
except:
import traceback
traceback.print_exc()
# Ensure that the pre-metadata file size is present. If it isn't,
# write 0 so that the file is rescanned
szpath = os.path.join(tdir, 'size.txt')
try:
with open(szpath, 'rb') as f:
int(f.read())
except:
with open(szpath, 'wb') as f:
f.write(b'0')
opfpath = os.path.join(tdir, 'metadata.opf')
try:
if os.stat(opfpath).st_size < 30:
raise Exception('metadata reading failed')
except:
mi = metadata_from_filename(fname)
with open(opfpath, 'wb') as f:
f.write(metadata_to_opf(mi))
self.staging.add(fname)
data.append((fname, tdir))
if data:
self.callback(data)
class AutoAdder(QObject):
metadata_read = pyqtSignal(object)
auto_convert = pyqtSignal(object)
def __init__(self, path, parent):
QObject.__init__(self, parent)
if path and os.path.isdir(path) and os.access(path, os.R_OK|os.W_OK):
self.watcher = QFileSystemWatcher(self)
self.worker = Worker(path, self.metadata_read.emit)
self.watcher.directoryChanged.connect(self.dir_changed,
type=Qt.ConnectionType.QueuedConnection)
self.metadata_read.connect(self.add_to_db,
type=Qt.ConnectionType.QueuedConnection)
QTimer.singleShot(2000, self.initialize)
self.auto_convert.connect(self.do_auto_convert,
type=Qt.ConnectionType.QueuedConnection)
elif path:
prints(path,
'is not a valid directory to watch for new ebooks, ignoring')
def read_rules(self):
if hasattr(self, 'worker'):
self.worker.read_rules()
def initialize(self):
try:
if os.listdir(self.worker.path):
self.dir_changed()
except:
pass
self.watcher.addPath(self.worker.path)
def dir_changed(self, *args):
if os.path.isdir(self.worker.path) and os.access(self.worker.path,
os.R_OK|os.W_OK):
if not self.worker.is_alive():
self.worker.start()
self.worker.wake_up.set()
def stop(self):
if hasattr(self, 'worker'):
self.worker.keep_running = False
self.worker.wake_up.set()
def wait(self):
if hasattr(self, 'worker'):
self.worker.join()
def __enter__(self):
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
def __exit__(self, *args):
QApplication.restoreOverrideCursor()
def add_to_db(self, data):
with self:
self.do_add(data)
def do_add(self, data):
from calibre.ebooks.metadata.opf2 import OPF
gui = self.parent()
if gui is None:
return
m = gui.library_view.model()
count = 0
needs_rescan = False
duplicates = []
added_ids = set()
for fname, tdir in data:
path_to_remove = os.path.join(self.worker.path, fname)
paths = [path_to_remove]
fpath = os.path.join(tdir, 'file_changed_by_plugins')
if os.path.exists(fpath):
with open(fpath) as f:
paths[0] = f.read()
sz = os.path.join(tdir, 'size.txt')
try:
with open(sz, 'rb') as f:
sz = int(f.read())
if sz != os.stat(paths[0]).st_size:
raise Exception('Looks like the file was written to after'
' we tried to read metadata')
except:
needs_rescan = True
try:
self.worker.staging.remove(fname)
except KeyError:
pass
continue
mi = os.path.join(tdir, 'metadata.opf')
if not os.access(mi, os.R_OK):
continue
mi = OPF(open(mi, 'rb'), tdir, populate_spine=False).to_book_metadata()
if gprefs.get('tag_map_on_add_rules'):
from calibre.ebooks.metadata.tag_mapper import map_tags
mi.tags = map_tags(mi.tags, gprefs['tag_map_on_add_rules'])
if gprefs.get('author_map_on_add_rules'):
from calibre.ebooks.metadata.author_mapper import (
compile_rules, map_authors
)
new_authors = map_authors(mi.authors, compile_rules(gprefs['author_map_on_add_rules']))
if new_authors != mi.authors:
mi.authors = new_authors
mi.author_sort = gui.current_db.new_api.author_sort_from_authors(mi.authors)
mi = [mi]
dups, ids = m.add_books(paths,
[os.path.splitext(fname)[1][1:].upper()], mi,
add_duplicates=not gprefs['auto_add_check_for_duplicates'],
return_ids=True)
added_ids |= set(ids)
num = len(ids)
if dups:
path = dups[0][0]
with open(os.path.join(tdir, 'dup_cache.'+dups[1][0].lower()),
'wb') as dest, open(path, 'rb') as src:
shutil.copyfileobj(src, dest)
dups[0][0] = dest.name
duplicates.append(dups)
try:
os.remove(path_to_remove)
self.worker.staging.remove(fname)
except:
import traceback
traceback.print_exc()
count += num
if duplicates:
paths, formats, metadata = [], [], []
for p, f, mis in duplicates:
paths.extend(p)
formats.extend(f)
metadata.extend(mis)
dups = [(mic, mic.cover, [p]) for mic, p in zip(metadata, paths)]
d = DuplicatesQuestion(m.db, dups, parent=gui)
dups = tuple(d.duplicates)
if dups:
paths, formats, metadata = [], [], []
for mi, cover, book_paths in dups:
paths.extend(book_paths)
formats.extend([p.rpartition('.')[-1] for p in book_paths])
metadata.extend([mi for i in book_paths])
ids = m.add_books(paths, formats, metadata,
add_duplicates=True, return_ids=True)[1]
added_ids |= set(ids)
num = len(ids)
count += num
for fname, tdir in data:
try:
shutil.rmtree(tdir)
except:
pass
if added_ids and gprefs['auto_add_auto_convert']:
self.auto_convert.emit(added_ids)
if count > 0:
m.books_added(count)
gui.status_bar.show_message(
(_('Added a book automatically from {src}') if count == 1 else _('Added {num} books automatically from {src}')).format(
num=count, src=self.worker.path), 2000)
gui.refresh_cover_browser()
if needs_rescan:
QTimer.singleShot(2000, self.dir_changed)
def do_auto_convert(self, added_ids):
gui = self.parent()
gui.iactions['Convert Books'].auto_convert_auto_add(added_ids)