Merge new worker thread
commit
9e5cad0dc8
@ -0,0 +1,207 @@
|
|||||||
|
|
||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import threading
|
||||||
|
import abc
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from cps import calibre_db
|
||||||
|
from cps import logger
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
# task 'status' consts
|
||||||
|
STAT_WAITING = 0
|
||||||
|
STAT_FAIL = 1
|
||||||
|
STAT_STARTED = 2
|
||||||
|
STAT_FINISH_SUCCESS = 3
|
||||||
|
|
||||||
|
# Only retain this many tasks in dequeued list
|
||||||
|
TASK_CLEANUP_TRIGGER = 20
|
||||||
|
|
||||||
|
QueuedTask = namedtuple('QueuedTask', 'num, user, added, task')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_main_thread():
|
||||||
|
for t in threading.enumerate():
|
||||||
|
if t.__class__.__name__ == '_MainThread':
|
||||||
|
return t
|
||||||
|
raise Exception("main thread not found?!")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ImprovedQueue(queue.Queue):
|
||||||
|
def to_list(self):
|
||||||
|
"""
|
||||||
|
Returns a copy of all items in the queue without removing them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.mutex:
|
||||||
|
return list(self.queue)
|
||||||
|
|
||||||
|
#Class for all worker tasks in the background
|
||||||
|
class WorkerThread(threading.Thread):
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getInstance(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = WorkerThread()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
|
self.dequeued = list()
|
||||||
|
|
||||||
|
self.doLock = threading.Lock()
|
||||||
|
self.queue = ImprovedQueue()
|
||||||
|
self.num = 0
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add(cls, user, task):
|
||||||
|
ins = cls.getInstance()
|
||||||
|
ins.num += 1
|
||||||
|
ins.queue.put(QueuedTask(
|
||||||
|
num=ins.num,
|
||||||
|
user=user,
|
||||||
|
added=datetime.now(),
|
||||||
|
task=task,
|
||||||
|
))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks(self):
|
||||||
|
with self.doLock:
|
||||||
|
tasks = self.queue.to_list() + self.dequeued
|
||||||
|
return sorted(tasks, key=lambda x: x.num)
|
||||||
|
|
||||||
|
|
||||||
|
# Main thread loop starting the different tasks
|
||||||
|
def run(self):
|
||||||
|
main_thread = _get_main_thread()
|
||||||
|
while main_thread.is_alive():
|
||||||
|
item = self.queue.get()
|
||||||
|
|
||||||
|
with self.doLock:
|
||||||
|
# once we hit our trigger, start cleaning up dead tasks
|
||||||
|
if len(self.dequeued) > TASK_CLEANUP_TRIGGER:
|
||||||
|
dead = []
|
||||||
|
alive = []
|
||||||
|
for x in self.dequeued:
|
||||||
|
(dead if x.task.dead else alive).append(x)
|
||||||
|
|
||||||
|
# if the ones that we need to keep are within the trigger, do nothing else
|
||||||
|
delta = len(self.dequeued) - len(dead)
|
||||||
|
if delta > TASK_CLEANUP_TRIGGER:
|
||||||
|
ret = alive
|
||||||
|
else:
|
||||||
|
# otherwise, lop off the oldest dead tasks until we hit the target trigger
|
||||||
|
ret = sorted(dead, key=lambda x: x.task.end_time)[-TASK_CLEANUP_TRIGGER:] + alive
|
||||||
|
|
||||||
|
self.dequeued = sorted(ret, key=lambda x: x.num)
|
||||||
|
# add to list so that in-progress tasks show up
|
||||||
|
self.dequeued.append(item)
|
||||||
|
|
||||||
|
# sometimes tasks (like Upload) don't actually have work to do and are created as already finished
|
||||||
|
if item.task.stat is STAT_WAITING:
|
||||||
|
# CalibreTask.start() should wrap all exceptions in it's own error handling
|
||||||
|
item.task.start(self)
|
||||||
|
|
||||||
|
self.queue.task_done()
|
||||||
|
|
||||||
|
|
||||||
|
class CalibreTask:
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self._progress = 0
|
||||||
|
self.stat = STAT_WAITING
|
||||||
|
self.error = None
|
||||||
|
self.start_time = None
|
||||||
|
self.end_time = None
|
||||||
|
self.message = message
|
||||||
|
self.id = uuid.uuid4()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def run(self, worker_thread):
|
||||||
|
"""Provides the caller some human-readable name for this class"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def name(self):
|
||||||
|
"""Provides the caller some human-readable name for this class"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def start(self, *args):
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
self.stat = STAT_STARTED
|
||||||
|
|
||||||
|
# catch any unhandled exceptions in a task and automatically fail it
|
||||||
|
try:
|
||||||
|
self.run(*args)
|
||||||
|
except Exception as e:
|
||||||
|
self._handleError(str(e))
|
||||||
|
log.exception(e)
|
||||||
|
|
||||||
|
self.end_time = datetime.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stat(self):
|
||||||
|
return self._stat
|
||||||
|
|
||||||
|
@stat.setter
|
||||||
|
def stat(self, x):
|
||||||
|
self._stat = x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def progress(self):
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
@progress.setter
|
||||||
|
def progress(self, x):
|
||||||
|
if not 0 <= x <= 1:
|
||||||
|
raise ValueError("Task progress should within [0, 1] range")
|
||||||
|
self._progress = x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error(self):
|
||||||
|
return self._error
|
||||||
|
|
||||||
|
@error.setter
|
||||||
|
def error(self, x):
|
||||||
|
self._error = x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def runtime(self):
|
||||||
|
return (self.end_time or datetime.now()) - self.start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dead(self):
|
||||||
|
"""Determines whether or not this task can be garbage collected
|
||||||
|
|
||||||
|
We have a separate dictating this because there may be certain tasks that want to override this
|
||||||
|
"""
|
||||||
|
# By default, we're good to clean a task if it's "Done"
|
||||||
|
return self.stat in (STAT_FINISH_SUCCESS, STAT_FAIL)
|
||||||
|
|
||||||
|
@progress.setter
|
||||||
|
def progress(self, x):
|
||||||
|
# todo: throw error if outside of [0,1]
|
||||||
|
self._progress = x
|
||||||
|
|
||||||
|
def _handleError(self, error_message):
|
||||||
|
log.error(error_message)
|
||||||
|
self.stat = STAT_FAIL
|
||||||
|
self.progress = 1
|
||||||
|
self.error = error_message
|
||||||
|
|
||||||
|
def _handleSuccess(self):
|
||||||
|
self.stat = STAT_FINISH_SUCCESS
|
||||||
|
self.progress = 1
|
@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from glob import glob
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
|
||||||
|
from cps import calibre_db, db
|
||||||
|
from cps import logger, config
|
||||||
|
from cps.subproc_wrapper import process_open
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
from cps.tasks.email import TaskEmail
|
||||||
|
from cps import gdriveutils
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskConvert(CalibreTask):
|
||||||
|
def __init__(self, file_path, bookid, taskMessage, settings, kindle_mail):
|
||||||
|
super(TaskConvert, self).__init__(taskMessage)
|
||||||
|
self.file_path = file_path
|
||||||
|
self.bookid = bookid
|
||||||
|
self.settings = settings
|
||||||
|
self.kindle_mail = kindle_mail
|
||||||
|
|
||||||
|
self.results = dict()
|
||||||
|
|
||||||
|
def run(self, worker_thread):
|
||||||
|
self.worker_thread = worker_thread
|
||||||
|
filename = self._convert_ebook_format()
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
if self.kindle_mail:
|
||||||
|
# if we're sending to kindle after converting, create a one-off task and run it immediately
|
||||||
|
# todo: figure out how to incorporate this into the progress
|
||||||
|
try:
|
||||||
|
task = TaskEmail(self.settings['subject'], self.results["path"],
|
||||||
|
filename, self.settings, self.kindle_mail,
|
||||||
|
self.settings['subject'], self.settings['body'], internal=True)
|
||||||
|
task.start(worker_thread)
|
||||||
|
|
||||||
|
# even though the convert task might be finished, if this task fails, fail the whole thing
|
||||||
|
if task.stat != STAT_FINISH_SUCCESS:
|
||||||
|
raise Exception(task.error)
|
||||||
|
except Exception as e:
|
||||||
|
return self._handleError(str(e))
|
||||||
|
|
||||||
|
def _convert_ebook_format(self):
|
||||||
|
error_message = None
|
||||||
|
file_path = self.file_path
|
||||||
|
book_id = self.bookid
|
||||||
|
format_old_ext = u'.' + self.settings['old_book_format'].lower()
|
||||||
|
format_new_ext = u'.' + self.settings['new_book_format'].lower()
|
||||||
|
|
||||||
|
# check to see if destination format already exists -
|
||||||
|
# if it does - mark the conversion task as complete and return a success
|
||||||
|
# this will allow send to kindle workflow to continue to work
|
||||||
|
if os.path.isfile(file_path + format_new_ext):
|
||||||
|
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
||||||
|
cur_book = calibre_db.get_book(book_id)
|
||||||
|
self.results['path'] = file_path
|
||||||
|
self.results['title'] = cur_book.title
|
||||||
|
self._handleSuccess()
|
||||||
|
return file_path + format_new_ext
|
||||||
|
else:
|
||||||
|
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
||||||
|
book_id,
|
||||||
|
format_new_ext)
|
||||||
|
|
||||||
|
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
|
||||||
|
check, error_message = self._convert_kepubify(file_path,
|
||||||
|
format_old_ext,
|
||||||
|
format_new_ext)
|
||||||
|
else:
|
||||||
|
# check if calibre converter-executable is existing
|
||||||
|
if not os.path.exists(config.config_converterpath):
|
||||||
|
# ToDo Text is not translated
|
||||||
|
self._handleError(_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
||||||
|
return
|
||||||
|
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext)
|
||||||
|
|
||||||
|
if check == 0:
|
||||||
|
cur_book = calibre_db.get_book(book_id)
|
||||||
|
if os.path.isfile(file_path + format_new_ext):
|
||||||
|
# self.db_queue.join()
|
||||||
|
new_format = db.Data(name=cur_book.data[0].name,
|
||||||
|
book_format=self.settings['new_book_format'].upper(),
|
||||||
|
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
||||||
|
|
||||||
|
cur_book.data.append(new_format)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# db.session.merge(cur_book)
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
log.error("Database error: %s", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.results['path'] = cur_book.path
|
||||||
|
self.results['title'] = cur_book.title
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
os.remove(file_path + format_old_ext)
|
||||||
|
self._handleSuccess()
|
||||||
|
return file_path + format_new_ext
|
||||||
|
else:
|
||||||
|
error_message = _('%(format)s format not found on disk', format=format_new_ext.upper())
|
||||||
|
log.info("ebook converter failed with error while converting book")
|
||||||
|
if not error_message:
|
||||||
|
error_message = _('Ebook converter failed with unknown error')
|
||||||
|
self._handleError(error_message)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext):
|
||||||
|
quotes = [1, 3]
|
||||||
|
command = [config.config_kepubifypath, (file_path + format_old_ext), '-o', os.path.dirname(file_path)]
|
||||||
|
try:
|
||||||
|
p = process_open(command, quotes)
|
||||||
|
except OSError as e:
|
||||||
|
return 1, _(u"Kepubify-converter failed: %(error)s", error=e)
|
||||||
|
self.progress = 0.01
|
||||||
|
while True:
|
||||||
|
nextline = p.stdout.readlines()
|
||||||
|
nextline = [x.strip('\n') for x in nextline if x != '\n']
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
nextline = [x.decode('utf-8') for x in nextline]
|
||||||
|
for line in nextline:
|
||||||
|
log.debug(line)
|
||||||
|
if p.poll() is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# ToD Handle
|
||||||
|
# process returncode
|
||||||
|
check = p.returncode
|
||||||
|
|
||||||
|
# move file
|
||||||
|
if check == 0:
|
||||||
|
converted_file = glob(os.path.join(os.path.dirname(file_path), "*.kepub.epub"))
|
||||||
|
if len(converted_file) == 1:
|
||||||
|
copyfile(converted_file[0], (file_path + format_new_ext))
|
||||||
|
os.unlink(converted_file[0])
|
||||||
|
else:
|
||||||
|
return 1, _(u"Converted file not found or more than one file in folder %(folder)s",
|
||||||
|
folder=os.path.dirname(file_path))
|
||||||
|
return check, None
|
||||||
|
|
||||||
|
def _convert_calibre(self, file_path, format_old_ext, format_new_ext):
|
||||||
|
try:
|
||||||
|
# Linux py2.7 encode as list without quotes no empty element for parameters
|
||||||
|
# linux py3.x no encode and as list without quotes no empty element for parameters
|
||||||
|
# windows py2.7 encode as string with quotes empty element for parameters is okay
|
||||||
|
# windows py 3.x no encode and as string with quotes empty element for parameters is okay
|
||||||
|
# separate handling for windows and linux
|
||||||
|
quotes = [1, 2]
|
||||||
|
command = [config.config_converterpath, (file_path + format_old_ext),
|
||||||
|
(file_path + format_new_ext)]
|
||||||
|
quotes_index = 3
|
||||||
|
if config.config_calibre:
|
||||||
|
parameters = config.config_calibre.split(" ")
|
||||||
|
for param in parameters:
|
||||||
|
command.append(param)
|
||||||
|
quotes.append(quotes_index)
|
||||||
|
quotes_index += 1
|
||||||
|
|
||||||
|
p = process_open(command, quotes)
|
||||||
|
except OSError as e:
|
||||||
|
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
|
||||||
|
|
||||||
|
while p.poll() is None:
|
||||||
|
nextline = p.stdout.readline()
|
||||||
|
if os.name == 'nt' and sys.version_info < (3, 0):
|
||||||
|
nextline = nextline.decode('windows-1252')
|
||||||
|
elif os.name == 'posix' and sys.version_info < (3, 0):
|
||||||
|
nextline = nextline.decode('utf-8')
|
||||||
|
log.debug(nextline.strip('\r\n'))
|
||||||
|
# parse progress string from calibre-converter
|
||||||
|
progress = re.search(r"(\d+)%\s.*", nextline)
|
||||||
|
if progress:
|
||||||
|
self.progress = int(progress.group(1)) / 100
|
||||||
|
|
||||||
|
# process returncode
|
||||||
|
check = p.returncode
|
||||||
|
calibre_traceback = p.stderr.readlines()
|
||||||
|
error_message = ""
|
||||||
|
for ele in calibre_traceback:
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
ele = ele.decode('utf-8')
|
||||||
|
log.debug(ele.strip('\n'))
|
||||||
|
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
||||||
|
error_message = _("Calibre failed with error: %(error)s", ele.strip('\n'))
|
||||||
|
return check, error_message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "Convert"
|
@ -0,0 +1,238 @@
|
|||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
|
||||||
|
try:
|
||||||
|
from StringIO import StringIO
|
||||||
|
from email.MIMEBase import MIMEBase
|
||||||
|
from email.MIMEMultipart import MIMEMultipart
|
||||||
|
from email.MIMEText import MIMEText
|
||||||
|
except ImportError:
|
||||||
|
from io import StringIO
|
||||||
|
from email.mime.base import MIMEBase
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
from email import encoders
|
||||||
|
from email.utils import formatdate, make_msgid
|
||||||
|
from email.generator import Generator
|
||||||
|
|
||||||
|
from cps.services.worker import CalibreTask
|
||||||
|
from cps import logger, config
|
||||||
|
|
||||||
|
from cps import gdriveutils
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
CHUNKSIZE = 8192
|
||||||
|
|
||||||
|
|
||||||
|
# Class for sending email with ability to get current progress
|
||||||
|
class EmailBase:
|
||||||
|
|
||||||
|
transferSize = 0
|
||||||
|
progress = 0
|
||||||
|
|
||||||
|
def data(self, msg):
|
||||||
|
self.transferSize = len(msg)
|
||||||
|
(code, resp) = smtplib.SMTP.data(self, msg)
|
||||||
|
self.progress = 0
|
||||||
|
return (code, resp)
|
||||||
|
|
||||||
|
def send(self, strg):
|
||||||
|
"""Send `strg' to the server."""
|
||||||
|
log.debug('send: %r', strg[:300])
|
||||||
|
if hasattr(self, 'sock') and self.sock:
|
||||||
|
try:
|
||||||
|
if self.transferSize:
|
||||||
|
lock=threading.Lock()
|
||||||
|
lock.acquire()
|
||||||
|
self.transferSize = len(strg)
|
||||||
|
lock.release()
|
||||||
|
for i in range(0, self.transferSize, CHUNKSIZE):
|
||||||
|
if isinstance(strg, bytes):
|
||||||
|
self.sock.send((strg[i:i + CHUNKSIZE]))
|
||||||
|
else:
|
||||||
|
self.sock.send((strg[i:i + CHUNKSIZE]).encode('utf-8'))
|
||||||
|
lock.acquire()
|
||||||
|
self.progress = i
|
||||||
|
lock.release()
|
||||||
|
else:
|
||||||
|
self.sock.sendall(strg.encode('utf-8'))
|
||||||
|
except socket.error:
|
||||||
|
self.close()
|
||||||
|
raise smtplib.SMTPServerDisconnected('Server not connected')
|
||||||
|
else:
|
||||||
|
raise smtplib.SMTPServerDisconnected('please run connect() first')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _print_debug(cls, *args):
|
||||||
|
log.debug(args)
|
||||||
|
|
||||||
|
def getTransferStatus(self):
|
||||||
|
if self.transferSize:
|
||||||
|
lock2 = threading.Lock()
|
||||||
|
lock2.acquire()
|
||||||
|
value = int((float(self.progress) / float(self.transferSize))*100)
|
||||||
|
lock2.release()
|
||||||
|
return value / 100
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
# Class for sending email with ability to get current progress, derived from emailbase class
|
||||||
|
class Email(EmailBase, smtplib.SMTP):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
smtplib.SMTP.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
|
||||||
|
class EmailSSL(EmailBase, smtplib.SMTP_SSL):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
smtplib.SMTP_SSL.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskEmail(CalibreTask):
|
||||||
|
def __init__(self, subject, filepath, attachment, settings, recipient, taskMessage, text, internal=False):
|
||||||
|
super(TaskEmail, self).__init__(taskMessage)
|
||||||
|
self.subject = subject
|
||||||
|
self.attachment = attachment
|
||||||
|
self.settings = settings
|
||||||
|
self.filepath = filepath
|
||||||
|
self.recipent = recipient
|
||||||
|
self.text = text
|
||||||
|
self.asyncSMTP = None
|
||||||
|
|
||||||
|
self.results = dict()
|
||||||
|
|
||||||
|
def run(self, worker_thread):
|
||||||
|
# create MIME message
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['Subject'] = self.subject
|
||||||
|
msg['Message-Id'] = make_msgid('calibre-web')
|
||||||
|
msg['Date'] = formatdate(localtime=True)
|
||||||
|
text = self.text
|
||||||
|
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
|
||||||
|
if self.attachment:
|
||||||
|
result = self._get_attachment(self.filepath, self.attachment)
|
||||||
|
if result:
|
||||||
|
msg.attach(result)
|
||||||
|
else:
|
||||||
|
self._handleError(u"Attachment not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg['From'] = self.settings["mail_from"]
|
||||||
|
msg['To'] = self.recipent
|
||||||
|
|
||||||
|
use_ssl = int(self.settings.get('mail_use_ssl', 0))
|
||||||
|
try:
|
||||||
|
# convert MIME message to string
|
||||||
|
fp = StringIO()
|
||||||
|
gen = Generator(fp, mangle_from_=False)
|
||||||
|
gen.flatten(msg)
|
||||||
|
msg = fp.getvalue()
|
||||||
|
|
||||||
|
# send email
|
||||||
|
timeout = 600 # set timeout to 5mins
|
||||||
|
|
||||||
|
# redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
|
||||||
|
# _print_debug function
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
org_smtpstderr = smtplib.stderr
|
||||||
|
smtplib.stderr = logger.StderrLogger('worker.smtp')
|
||||||
|
|
||||||
|
if use_ssl == 2:
|
||||||
|
self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"],
|
||||||
|
timeout=timeout)
|
||||||
|
else:
|
||||||
|
self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout)
|
||||||
|
|
||||||
|
# link to logginglevel
|
||||||
|
if logger.is_debug_enabled():
|
||||||
|
self.asyncSMTP.set_debuglevel(1)
|
||||||
|
if use_ssl == 1:
|
||||||
|
self.asyncSMTP.starttls()
|
||||||
|
if self.settings["mail_password"]:
|
||||||
|
self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"]))
|
||||||
|
self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, msg)
|
||||||
|
self.asyncSMTP.quit()
|
||||||
|
self._handleSuccess()
|
||||||
|
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
smtplib.stderr = org_smtpstderr
|
||||||
|
|
||||||
|
except (MemoryError) as e:
|
||||||
|
log.exception(e)
|
||||||
|
self._handleError(u'MemoryError sending email: ' + str(e))
|
||||||
|
return None
|
||||||
|
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||||
|
if hasattr(e, "smtp_error"):
|
||||||
|
text = e.smtp_error.decode('utf-8').replace("\n", '. ')
|
||||||
|
elif hasattr(e, "message"):
|
||||||
|
text = e.message
|
||||||
|
else:
|
||||||
|
log.exception(e)
|
||||||
|
text = ''
|
||||||
|
self._handleError(u'Smtplib Error sending email: ' + text)
|
||||||
|
return None
|
||||||
|
except (socket.error) as e:
|
||||||
|
self._handleError(u'Socket Error sending email: ' + e.strerror)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def progress(self):
|
||||||
|
if self.asyncSMTP is not None:
|
||||||
|
return self.asyncSMTP.getTransferStatus()
|
||||||
|
else:
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
@progress.setter
|
||||||
|
def progress(self, x):
|
||||||
|
"""This gets explicitly set when handle(Success|Error) are called. In this case, remove the SMTP connection"""
|
||||||
|
if x == 1:
|
||||||
|
self.asyncSMTP = None
|
||||||
|
self._progress = x
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_attachment(cls, bookpath, filename):
|
||||||
|
"""Get file as MIMEBase message"""
|
||||||
|
calibrepath = config.config_calibre_dir
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
df = gdriveutils.getFileFromEbooksFolder(bookpath, filename)
|
||||||
|
if df:
|
||||||
|
datafile = os.path.join(calibrepath, bookpath, filename)
|
||||||
|
if not os.path.exists(os.path.join(calibrepath, bookpath)):
|
||||||
|
os.makedirs(os.path.join(calibrepath, bookpath))
|
||||||
|
df.GetContentFile(datafile)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
file_ = open(datafile, 'rb')
|
||||||
|
data = file_.read()
|
||||||
|
file_.close()
|
||||||
|
os.remove(datafile)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
|
||||||
|
data = file_.read()
|
||||||
|
file_.close()
|
||||||
|
except IOError as e:
|
||||||
|
log.exception(e)
|
||||||
|
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
||||||
|
return None
|
||||||
|
|
||||||
|
attachment = MIMEBase('application', 'octet-stream')
|
||||||
|
attachment.set_payload(data)
|
||||||
|
encoders.encode_base64(attachment)
|
||||||
|
attachment.add_header('Content-Disposition', 'attachment',
|
||||||
|
filename=filename)
|
||||||
|
return attachment
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "Email"
|
@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from cps.services.worker import CalibreTask, STAT_FINISH_SUCCESS
|
||||||
|
|
||||||
|
class TaskUpload(CalibreTask):
|
||||||
|
def __init__(self, taskMessage):
|
||||||
|
super(TaskUpload, self).__init__(taskMessage)
|
||||||
|
self.start_time = self.end_time = datetime.now()
|
||||||
|
self.stat = STAT_FINISH_SUCCESS
|
||||||
|
self.progress = 1
|
||||||
|
|
||||||
|
def run(self, worker_thread):
|
||||||
|
"""Upload task doesn't have anything to do, it's simply a way to add information to the task list"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "Upload"
|
@ -1,602 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
|
|
||||||
# Copyright (C) 2018-2019 OzzieIsaacs, bodybybuddha, janeczku
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import smtplib
|
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
try:
|
|
||||||
import queue
|
|
||||||
except ImportError:
|
|
||||||
import Queue as queue
|
|
||||||
from glob import glob
|
|
||||||
from shutil import copyfile
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
try:
|
|
||||||
from StringIO import StringIO
|
|
||||||
from email.MIMEBase import MIMEBase
|
|
||||||
from email.MIMEMultipart import MIMEMultipart
|
|
||||||
from email.MIMEText import MIMEText
|
|
||||||
except ImportError:
|
|
||||||
from io import StringIO
|
|
||||||
from email.mime.base import MIMEBase
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
|
|
||||||
from email import encoders
|
|
||||||
from email.utils import formatdate
|
|
||||||
from email.utils import make_msgid
|
|
||||||
from email.generator import Generator
|
|
||||||
from flask_babel import gettext as _
|
|
||||||
|
|
||||||
from . import calibre_db, db
|
|
||||||
from . import logger, config
|
|
||||||
from .subproc_wrapper import process_open
|
|
||||||
from . import gdriveutils
|
|
||||||
|
|
||||||
log = logger.create()
|
|
||||||
|
|
||||||
chunksize = 8192
|
|
||||||
# task 'status' consts
|
|
||||||
STAT_WAITING = 0
|
|
||||||
STAT_FAIL = 1
|
|
||||||
STAT_STARTED = 2
|
|
||||||
STAT_FINISH_SUCCESS = 3
|
|
||||||
#taskType consts
|
|
||||||
TASK_EMAIL = 1
|
|
||||||
TASK_CONVERT = 2
|
|
||||||
TASK_UPLOAD = 3
|
|
||||||
TASK_CONVERT_ANY = 4
|
|
||||||
|
|
||||||
RET_FAIL = 0
|
|
||||||
RET_SUCCESS = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _get_main_thread():
|
|
||||||
for t in threading.enumerate():
|
|
||||||
if t.__class__.__name__ == '_MainThread':
|
|
||||||
return t
|
|
||||||
raise Exception("main thread not found?!")
|
|
||||||
|
|
||||||
|
|
||||||
# For gdrive download book from gdrive to calibredir (temp dir for books), read contents in both cases and append
|
|
||||||
# it in MIME Base64 encoded to
|
|
||||||
def get_attachment(bookpath, filename):
|
|
||||||
"""Get file as MIMEBase message"""
|
|
||||||
calibrepath = config.config_calibre_dir
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
df = gdriveutils.getFileFromEbooksFolder(bookpath, filename)
|
|
||||||
if df:
|
|
||||||
datafile = os.path.join(calibrepath, bookpath, filename)
|
|
||||||
if not os.path.exists(os.path.join(calibrepath, bookpath)):
|
|
||||||
os.makedirs(os.path.join(calibrepath, bookpath))
|
|
||||||
df.GetContentFile(datafile)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
file_ = open(datafile, 'rb')
|
|
||||||
data = file_.read()
|
|
||||||
file_.close()
|
|
||||||
os.remove(datafile)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb')
|
|
||||||
data = file_.read()
|
|
||||||
file_.close()
|
|
||||||
except IOError as e:
|
|
||||||
log.exception(e)
|
|
||||||
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
|
||||||
return None
|
|
||||||
|
|
||||||
attachment = MIMEBase('application', 'octet-stream')
|
|
||||||
attachment.set_payload(data)
|
|
||||||
encoders.encode_base64(attachment)
|
|
||||||
attachment.add_header('Content-Disposition', 'attachment',
|
|
||||||
filename=filename)
|
|
||||||
return attachment
|
|
||||||
|
|
||||||
|
|
||||||
# Class for sending email with ability to get current progress
|
|
||||||
class emailbase():
|
|
||||||
|
|
||||||
transferSize = 0
|
|
||||||
progress = 0
|
|
||||||
|
|
||||||
def data(self, msg):
|
|
||||||
self.transferSize = len(msg)
|
|
||||||
(code, resp) = smtplib.SMTP.data(self, msg)
|
|
||||||
self.progress = 0
|
|
||||||
return (code, resp)
|
|
||||||
|
|
||||||
def send(self, strg):
|
|
||||||
"""Send `strg' to the server."""
|
|
||||||
log.debug('send: %r', strg[:300])
|
|
||||||
if hasattr(self, 'sock') and self.sock:
|
|
||||||
try:
|
|
||||||
if self.transferSize:
|
|
||||||
lock=threading.Lock()
|
|
||||||
lock.acquire()
|
|
||||||
self.transferSize = len(strg)
|
|
||||||
lock.release()
|
|
||||||
for i in range(0, self.transferSize, chunksize):
|
|
||||||
if isinstance(strg, bytes):
|
|
||||||
self.sock.send((strg[i:i+chunksize]))
|
|
||||||
else:
|
|
||||||
self.sock.send((strg[i:i + chunksize]).encode('utf-8'))
|
|
||||||
lock.acquire()
|
|
||||||
self.progress = i
|
|
||||||
lock.release()
|
|
||||||
else:
|
|
||||||
self.sock.sendall(strg.encode('utf-8'))
|
|
||||||
except socket.error:
|
|
||||||
self.close()
|
|
||||||
raise smtplib.SMTPServerDisconnected('Server not connected')
|
|
||||||
else:
|
|
||||||
raise smtplib.SMTPServerDisconnected('please run connect() first')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _print_debug(self, *args):
|
|
||||||
log.debug(args)
|
|
||||||
|
|
||||||
def getTransferStatus(self):
|
|
||||||
if self.transferSize:
|
|
||||||
lock2 = threading.Lock()
|
|
||||||
lock2.acquire()
|
|
||||||
value = int((float(self.progress) / float(self.transferSize))*100)
|
|
||||||
lock2.release()
|
|
||||||
return str(value) + ' %'
|
|
||||||
else:
|
|
||||||
return "100 %"
|
|
||||||
|
|
||||||
|
|
||||||
# Class for sending email with ability to get current progress, derived from emailbase class
|
|
||||||
class email(emailbase, smtplib.SMTP):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
smtplib.SMTP.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# Class for sending ssl encrypted email with ability to get current progress, , derived from emailbase class
|
|
||||||
class email_SSL(emailbase, smtplib.SMTP_SSL):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
smtplib.SMTP_SSL.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
#Class for all worker tasks in the background
|
|
||||||
class WorkerThread(threading.Thread):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.status = 0
|
|
||||||
self.current = 0
|
|
||||||
self.last = 0
|
|
||||||
self.queue = list()
|
|
||||||
self.UIqueue = list()
|
|
||||||
self.asyncSMTP = None
|
|
||||||
self.id = 0
|
|
||||||
self.db_queue = queue.Queue()
|
|
||||||
calibre_db.add_queue(self.db_queue)
|
|
||||||
self.doLock = threading.Lock()
|
|
||||||
|
|
||||||
# Main thread loop starting the different tasks
|
|
||||||
def run(self):
|
|
||||||
main_thread = _get_main_thread()
|
|
||||||
while main_thread.is_alive():
|
|
||||||
try:
|
|
||||||
self.doLock.acquire()
|
|
||||||
if self.current != self.last:
|
|
||||||
index = self.current
|
|
||||||
log.info(index)
|
|
||||||
log.info(len(self.queue))
|
|
||||||
self.doLock.release()
|
|
||||||
if self.queue[index]['taskType'] == TASK_EMAIL:
|
|
||||||
self._send_raw_email()
|
|
||||||
elif self.queue[index]['taskType'] in (TASK_CONVERT, TASK_CONVERT_ANY):
|
|
||||||
self._convert_any_format()
|
|
||||||
# TASK_UPLOAD is handled implicitly
|
|
||||||
self.doLock.acquire()
|
|
||||||
self.current += 1
|
|
||||||
if self.current > self.last:
|
|
||||||
self.current = self.last
|
|
||||||
self.doLock.release()
|
|
||||||
else:
|
|
||||||
self.doLock.release()
|
|
||||||
except Exception as e:
|
|
||||||
log.exception(e)
|
|
||||||
self.doLock.release()
|
|
||||||
if main_thread.is_alive():
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def get_send_status(self):
|
|
||||||
if self.asyncSMTP:
|
|
||||||
return self.asyncSMTP.getTransferStatus()
|
|
||||||
else:
|
|
||||||
return "0 %"
|
|
||||||
|
|
||||||
def _delete_completed_tasks(self):
|
|
||||||
for index, task in reversed(list(enumerate(self.UIqueue))):
|
|
||||||
if task['progress'] == "100 %":
|
|
||||||
# delete tasks
|
|
||||||
self.queue.pop(index)
|
|
||||||
self.UIqueue.pop(index)
|
|
||||||
# if we are deleting entries before the current index, adjust the index
|
|
||||||
if index <= self.current and self.current:
|
|
||||||
self.current -= 1
|
|
||||||
self.last = len(self.queue)
|
|
||||||
|
|
||||||
def get_taskstatus(self):
|
|
||||||
self.doLock.acquire()
|
|
||||||
if self.current < len(self.queue):
|
|
||||||
if self.UIqueue[self.current]['stat'] == STAT_STARTED:
|
|
||||||
if self.queue[self.current]['taskType'] == TASK_EMAIL:
|
|
||||||
self.UIqueue[self.current]['progress'] = self.get_send_status()
|
|
||||||
self.UIqueue[self.current]['formRuntime'] = datetime.now() - self.queue[self.current]['starttime']
|
|
||||||
self.UIqueue[self.current]['rt'] = self.UIqueue[self.current]['formRuntime'].days*24*60 \
|
|
||||||
+ self.UIqueue[self.current]['formRuntime'].seconds \
|
|
||||||
+ self.UIqueue[self.current]['formRuntime'].microseconds
|
|
||||||
self.doLock.release()
|
|
||||||
return self.UIqueue
|
|
||||||
|
|
||||||
def _convert_any_format(self):
|
|
||||||
# convert book, and upload in case of google drive
|
|
||||||
self.doLock.acquire()
|
|
||||||
index = self.current
|
|
||||||
self.doLock.release()
|
|
||||||
self.UIqueue[index]['stat'] = STAT_STARTED
|
|
||||||
self.queue[index]['starttime'] = datetime.now()
|
|
||||||
self.UIqueue[index]['formStarttime'] = self.queue[index]['starttime']
|
|
||||||
curr_task = self.queue[index]['taskType']
|
|
||||||
filename = self._convert_ebook_format()
|
|
||||||
if filename:
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
|
||||||
if curr_task == TASK_CONVERT:
|
|
||||||
self.add_email(self.queue[index]['settings']['subject'], self.queue[index]['path'],
|
|
||||||
filename, self.queue[index]['settings'], self.queue[index]['kindle'],
|
|
||||||
self.UIqueue[index]['user'], self.queue[index]['title'],
|
|
||||||
self.queue[index]['settings']['body'], internal=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_ebook_format(self):
|
|
||||||
error_message = None
|
|
||||||
self.doLock.acquire()
|
|
||||||
index = self.current
|
|
||||||
self.doLock.release()
|
|
||||||
file_path = self.queue[index]['file_path']
|
|
||||||
book_id = self.queue[index]['bookid']
|
|
||||||
format_old_ext = u'.' + self.queue[index]['settings']['old_book_format'].lower()
|
|
||||||
format_new_ext = u'.' + self.queue[index]['settings']['new_book_format'].lower()
|
|
||||||
|
|
||||||
# check to see if destination format already exists -
|
|
||||||
# if it does - mark the conversion task as complete and return a success
|
|
||||||
# this will allow send to kindle workflow to continue to work
|
|
||||||
if os.path.isfile(file_path + format_new_ext):
|
|
||||||
log.info("Book id %d already converted to %s", book_id, format_new_ext)
|
|
||||||
cur_book = calibre_db.get_book(book_id)
|
|
||||||
self.queue[index]['path'] = file_path
|
|
||||||
self.queue[index]['title'] = cur_book.title
|
|
||||||
self._handleSuccess()
|
|
||||||
return file_path + format_new_ext
|
|
||||||
else:
|
|
||||||
log.info("Book id %d - target format of %s does not exist. Moving forward with convert.",
|
|
||||||
book_id,
|
|
||||||
format_new_ext)
|
|
||||||
|
|
||||||
if config.config_kepubifypath and format_old_ext == '.epub' and format_new_ext == '.kepub':
|
|
||||||
check, error_message = self._convert_kepubify(file_path,
|
|
||||||
format_old_ext,
|
|
||||||
format_new_ext,
|
|
||||||
index)
|
|
||||||
else:
|
|
||||||
# check if calibre converter-executable is existing
|
|
||||||
if not os.path.exists(config.config_converterpath):
|
|
||||||
# ToDo Text is not translated
|
|
||||||
self._handleError(_(u"Calibre ebook-convert %(tool)s not found", tool=config.config_converterpath))
|
|
||||||
return
|
|
||||||
check, error_message = self._convert_calibre(file_path, format_old_ext, format_new_ext, index)
|
|
||||||
|
|
||||||
if check == 0:
|
|
||||||
cur_book = calibre_db.get_book(book_id)
|
|
||||||
if os.path.isfile(file_path + format_new_ext):
|
|
||||||
# self.db_queue.join()
|
|
||||||
new_format = db.Data(name=cur_book.data[0].name,
|
|
||||||
book_format=self.queue[index]['settings']['new_book_format'].upper(),
|
|
||||||
book=book_id, uncompressed_size=os.path.getsize(file_path + format_new_ext))
|
|
||||||
task = {'task':'add_format','id': book_id, 'format': new_format}
|
|
||||||
self.db_queue.put(task)
|
|
||||||
# To Do how to handle error?
|
|
||||||
|
|
||||||
'''cur_book.data.append(new_format)
|
|
||||||
try:
|
|
||||||
# db.session.merge(cur_book)
|
|
||||||
calibre_db.session.commit()
|
|
||||||
except OperationalError as e:
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
log.error("Database error: %s", e)
|
|
||||||
self._handleError(_(u"Database error: %(error)s.", error=e))
|
|
||||||
return'''
|
|
||||||
|
|
||||||
self.queue[index]['path'] = cur_book.path
|
|
||||||
self.queue[index]['title'] = cur_book.title
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
os.remove(file_path + format_old_ext)
|
|
||||||
self._handleSuccess()
|
|
||||||
return file_path + format_new_ext
|
|
||||||
else:
|
|
||||||
error_message = format_new_ext.upper() + ' format not found on disk'
|
|
||||||
log.info("ebook converter failed with error while converting book")
|
|
||||||
if not error_message:
|
|
||||||
error_message = 'Ebook converter failed with unknown error'
|
|
||||||
self._handleError(error_message)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_calibre(self, file_path, format_old_ext, format_new_ext, index):
|
|
||||||
try:
|
|
||||||
# Linux py2.7 encode as list without quotes no empty element for parameters
|
|
||||||
# linux py3.x no encode and as list without quotes no empty element for parameters
|
|
||||||
# windows py2.7 encode as string with quotes empty element for parameters is okay
|
|
||||||
# windows py 3.x no encode and as string with quotes empty element for parameters is okay
|
|
||||||
# separate handling for windows and linux
|
|
||||||
quotes = [1, 2]
|
|
||||||
command = [config.config_converterpath, (file_path + format_old_ext),
|
|
||||||
(file_path + format_new_ext)]
|
|
||||||
quotes_index = 3
|
|
||||||
if config.config_calibre:
|
|
||||||
parameters = config.config_calibre.split(" ")
|
|
||||||
for param in parameters:
|
|
||||||
command.append(param)
|
|
||||||
quotes.append(quotes_index)
|
|
||||||
quotes_index += 1
|
|
||||||
|
|
||||||
p = process_open(command, quotes)
|
|
||||||
except OSError as e:
|
|
||||||
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
|
|
||||||
|
|
||||||
while p.poll() is None:
|
|
||||||
nextline = p.stdout.readline()
|
|
||||||
if os.name == 'nt' and sys.version_info < (3, 0):
|
|
||||||
nextline = nextline.decode('windows-1252')
|
|
||||||
elif os.name == 'posix' and sys.version_info < (3, 0):
|
|
||||||
nextline = nextline.decode('utf-8')
|
|
||||||
log.debug(nextline.strip('\r\n'))
|
|
||||||
# parse progress string from calibre-converter
|
|
||||||
progress = re.search(r"(\d+)%\s.*", nextline)
|
|
||||||
if progress:
|
|
||||||
self.UIqueue[index]['progress'] = progress.group(1) + ' %'
|
|
||||||
|
|
||||||
# process returncode
|
|
||||||
check = p.returncode
|
|
||||||
calibre_traceback = p.stderr.readlines()
|
|
||||||
error_message = ""
|
|
||||||
for ele in calibre_traceback:
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
ele = ele.decode('utf-8')
|
|
||||||
log.debug(ele.strip('\n'))
|
|
||||||
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
|
||||||
error_message = "Calibre failed with error: %s" % ele.strip('\n')
|
|
||||||
return check, error_message
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_kepubify(self, file_path, format_old_ext, format_new_ext, index):
|
|
||||||
quotes = [1, 3]
|
|
||||||
command = [config.config_kepubifypath, (file_path + format_old_ext), '-o', os.path.dirname(file_path)]
|
|
||||||
try:
|
|
||||||
p = process_open(command, quotes)
|
|
||||||
except OSError as e:
|
|
||||||
return 1, _(u"Kepubify-converter failed: %(error)s", error=e)
|
|
||||||
self.UIqueue[index]['progress'] = '1 %'
|
|
||||||
while True:
|
|
||||||
nextline = p.stdout.readlines()
|
|
||||||
nextline = [x.strip('\n') for x in nextline if x != '\n']
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
nextline = [x.decode('utf-8') for x in nextline]
|
|
||||||
for line in nextline:
|
|
||||||
log.debug(line)
|
|
||||||
if p.poll() is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
# ToD Handle
|
|
||||||
# process returncode
|
|
||||||
check = p.returncode
|
|
||||||
|
|
||||||
# move file
|
|
||||||
if check == 0:
|
|
||||||
converted_file = glob(os.path.join(os.path.dirname(file_path), "*.kepub.epub"))
|
|
||||||
if len(converted_file) == 1:
|
|
||||||
copyfile(converted_file[0], (file_path + format_new_ext))
|
|
||||||
os.unlink(converted_file[0])
|
|
||||||
else:
|
|
||||||
return 1, _(u"Converted file not found or more than one file in folder %(folder)s",
|
|
||||||
folder=os.path.dirname(file_path))
|
|
||||||
return check, None
|
|
||||||
|
|
||||||
|
|
||||||
def add_convert(self, file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
|
|
||||||
self.doLock.acquire()
|
|
||||||
if self.last >= 20:
|
|
||||||
self._delete_completed_tasks()
|
|
||||||
# progress, runtime, and status = 0
|
|
||||||
self.id += 1
|
|
||||||
task = TASK_CONVERT_ANY
|
|
||||||
if kindle_mail:
|
|
||||||
task = TASK_CONVERT
|
|
||||||
self.queue.append({'file_path':file_path, 'bookid':bookid, 'starttime': 0, 'kindle': kindle_mail,
|
|
||||||
'taskType': task, 'settings':settings})
|
|
||||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
|
|
||||||
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': task } )
|
|
||||||
|
|
||||||
self.last=len(self.queue)
|
|
||||||
self.doLock.release()
|
|
||||||
|
|
||||||
def add_email(self, subject, filepath, attachment, settings, recipient, user_name, taskMessage,
|
|
||||||
text, internal=False):
|
|
||||||
# if more than 20 entries in the list, clean the list
|
|
||||||
self.doLock.acquire()
|
|
||||||
if self.last >= 20:
|
|
||||||
self._delete_completed_tasks()
|
|
||||||
if internal:
|
|
||||||
self.current-= 1
|
|
||||||
# progress, runtime, and status = 0
|
|
||||||
self.id += 1
|
|
||||||
self.queue.append({'subject':subject, 'attachment':attachment, 'filepath':filepath,
|
|
||||||
'settings':settings, 'recipent':recipient, 'starttime': 0,
|
|
||||||
'taskType': TASK_EMAIL, 'text':text})
|
|
||||||
self.UIqueue.append({'user': user_name, 'formStarttime': '', 'progress': " 0 %", 'taskMess': taskMessage,
|
|
||||||
'runtime': '0 s', 'stat': STAT_WAITING,'id': self.id, 'taskType': TASK_EMAIL })
|
|
||||||
self.last=len(self.queue)
|
|
||||||
self.doLock.release()
|
|
||||||
|
|
||||||
def add_upload(self, user_name, taskMessage):
|
|
||||||
# if more than 20 entries in the list, clean the list
|
|
||||||
self.doLock.acquire()
|
|
||||||
|
|
||||||
|
|
||||||
if self.last >= 20:
|
|
||||||
self._delete_completed_tasks()
|
|
||||||
# progress=100%, runtime=0, and status finished
|
|
||||||
self.id += 1
|
|
||||||
starttime = datetime.now()
|
|
||||||
self.queue.append({'starttime': starttime, 'taskType': TASK_UPLOAD})
|
|
||||||
self.UIqueue.append({'user': user_name, 'formStarttime': starttime, 'progress': "100 %", 'taskMess': taskMessage,
|
|
||||||
'runtime': '0 s', 'stat': STAT_FINISH_SUCCESS,'id': self.id, 'taskType': TASK_UPLOAD})
|
|
||||||
self.last=len(self.queue)
|
|
||||||
self.doLock.release()
|
|
||||||
|
|
||||||
def _send_raw_email(self):
|
|
||||||
self.doLock.acquire()
|
|
||||||
index = self.current
|
|
||||||
self.doLock.release()
|
|
||||||
self.queue[index]['starttime'] = datetime.now()
|
|
||||||
self.UIqueue[index]['formStarttime'] = self.queue[index]['starttime']
|
|
||||||
self.UIqueue[index]['stat'] = STAT_STARTED
|
|
||||||
obj=self.queue[index]
|
|
||||||
# create MIME message
|
|
||||||
msg = MIMEMultipart()
|
|
||||||
msg['Subject'] = self.queue[index]['subject']
|
|
||||||
msg['Message-Id'] = make_msgid('calibre-web')
|
|
||||||
msg['Date'] = formatdate(localtime=True)
|
|
||||||
text = self.queue[index]['text']
|
|
||||||
msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8'))
|
|
||||||
if obj['attachment']:
|
|
||||||
result = get_attachment(obj['filepath'], obj['attachment'])
|
|
||||||
if result:
|
|
||||||
msg.attach(result)
|
|
||||||
else:
|
|
||||||
self._handleError(u"Attachment not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
msg['From'] = obj['settings']["mail_from"]
|
|
||||||
msg['To'] = obj['recipent']
|
|
||||||
|
|
||||||
use_ssl = int(obj['settings'].get('mail_use_ssl', 0))
|
|
||||||
try:
|
|
||||||
# convert MIME message to string
|
|
||||||
fp = StringIO()
|
|
||||||
gen = Generator(fp, mangle_from_=False)
|
|
||||||
gen.flatten(msg)
|
|
||||||
msg = fp.getvalue()
|
|
||||||
|
|
||||||
# send email
|
|
||||||
timeout = 600 # set timeout to 5mins
|
|
||||||
|
|
||||||
# redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten
|
|
||||||
# _print_debug function
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
org_smtpstderr = smtplib.stderr
|
|
||||||
smtplib.stderr = logger.StderrLogger('worker.smtp')
|
|
||||||
|
|
||||||
if use_ssl == 2:
|
|
||||||
self.asyncSMTP = email_SSL(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout=timeout)
|
|
||||||
else:
|
|
||||||
self.asyncSMTP = email(obj['settings']["mail_server"], obj['settings']["mail_port"], timeout=timeout)
|
|
||||||
|
|
||||||
# link to logginglevel
|
|
||||||
if logger.is_debug_enabled():
|
|
||||||
self.asyncSMTP.set_debuglevel(1)
|
|
||||||
if use_ssl == 1:
|
|
||||||
self.asyncSMTP.starttls()
|
|
||||||
if obj['settings']["mail_password"]:
|
|
||||||
self.asyncSMTP.login(str(obj['settings']["mail_login"]), str(obj['settings']["mail_password"]))
|
|
||||||
self.asyncSMTP.sendmail(obj['settings']["mail_from"], obj['recipent'], msg)
|
|
||||||
self.asyncSMTP.quit()
|
|
||||||
self._handleSuccess()
|
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
smtplib.stderr = org_smtpstderr
|
|
||||||
|
|
||||||
except (MemoryError) as e:
|
|
||||||
log.exception(e)
|
|
||||||
self._handleError(u'MemoryError sending email: ' + str(e))
|
|
||||||
return None
|
|
||||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
|
||||||
if hasattr(e, "smtp_error"):
|
|
||||||
text = e.smtp_error.decode('utf-8').replace("\n",'. ')
|
|
||||||
elif hasattr(e, "message"):
|
|
||||||
text = e.message
|
|
||||||
else:
|
|
||||||
log.exception(e)
|
|
||||||
text = ''
|
|
||||||
self._handleError(u'Smtplib Error sending email: ' + text)
|
|
||||||
return None
|
|
||||||
except (socket.error) as e:
|
|
||||||
self._handleError(u'Socket Error sending email: ' + e.strerror)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _handleError(self, error_message):
|
|
||||||
log.error(error_message)
|
|
||||||
self.doLock.acquire()
|
|
||||||
index = self.current
|
|
||||||
self.doLock.release()
|
|
||||||
self.UIqueue[index]['stat'] = STAT_FAIL
|
|
||||||
self.UIqueue[index]['progress'] = "100 %"
|
|
||||||
self.UIqueue[index]['formRuntime'] = datetime.now() - self.queue[index]['starttime']
|
|
||||||
self.UIqueue[index]['message'] = error_message
|
|
||||||
|
|
||||||
def _handleSuccess(self):
|
|
||||||
self.doLock.acquire()
|
|
||||||
index = self.current
|
|
||||||
self.doLock.release()
|
|
||||||
self.UIqueue[index]['stat'] = STAT_FINISH_SUCCESS
|
|
||||||
self.UIqueue[index]['progress'] = "100 %"
|
|
||||||
self.UIqueue[index]['formRuntime'] = datetime.now() - self.queue[index]['starttime']
|
|
||||||
|
|
||||||
|
|
||||||
def get_taskstatus():
|
|
||||||
return _worker.get_taskstatus()
|
|
||||||
|
|
||||||
|
|
||||||
def add_email(subject, filepath, attachment, settings, recipient, user_name, taskMessage, text):
|
|
||||||
return _worker.add_email(subject, filepath, attachment, settings, recipient, user_name, taskMessage, text)
|
|
||||||
|
|
||||||
|
|
||||||
def add_upload(user_name, taskMessage):
|
|
||||||
return _worker.add_upload(user_name, taskMessage)
|
|
||||||
|
|
||||||
|
|
||||||
def add_convert(file_path, bookid, user_name, taskMessage, settings, kindle_mail=None):
|
|
||||||
return _worker.add_convert(file_path, bookid, user_name, taskMessage, settings, kindle_mail)
|
|
||||||
|
|
||||||
|
|
||||||
_worker = WorkerThread()
|
|
||||||
_worker.start()
|
|
Loading…
Reference in New Issue