Merge remote-tracking branch 'upstream/master'
commit
5027304801
@ -0,0 +1,220 @@
|
|||||||
|
|
||||||
|
from __future__ import division, print_function, unicode_literals
|
||||||
|
import threading
|
||||||
|
import abc
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def cleanup_tasks(self):
|
||||||
|
with self.doLock:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Main thread loop starting the different tasks
|
||||||
|
def run(self):
|
||||||
|
main_thread = _get_main_thread()
|
||||||
|
while main_thread.is_alive():
|
||||||
|
try:
|
||||||
|
# this blocks until something is available. This can cause issues when the main thread dies - this
|
||||||
|
# thread will remain alive. We implement a timeout to unblock every second which allows us to check if
|
||||||
|
# the main thread is still alive.
|
||||||
|
# We don't use a daemon here because we don't want the tasks to just be abruptly halted, leading to
|
||||||
|
# possible file / database corruption
|
||||||
|
item = self.queue.get(timeout=1)
|
||||||
|
except queue.Empty as ex:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
with self.doLock:
|
||||||
|
# add to list so that in-progress tasks show up
|
||||||
|
self.dequeued.append(item)
|
||||||
|
|
||||||
|
# once we hit our trigger, start cleaning up dead tasks
|
||||||
|
if len(self.dequeued) > TASK_CLEANUP_TRIGGER:
|
||||||
|
self.cleanup_tasks()
|
||||||
|
|
||||||
|
# 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.exception(error_message)
|
||||||
|
self.stat = STAT_FAIL
|
||||||
|
self.progress = 1
|
||||||
|
self.error = error_message
|
||||||
|
|
||||||
|
def _handleSuccess(self):
|
||||||
|
self.stat = STAT_FINISH_SUCCESS
|
||||||
|
self.progress = 1
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,217 @@
|
|||||||
|
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.mail import TaskEmail
|
||||||
|
from cps import gdriveutils
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskConvert(CalibreTask):
|
||||||
|
def __init__(self, file_path, bookid, taskMessage, settings, kindle_mail, user=None):
|
||||||
|
super(TaskConvert, self).__init__(taskMessage)
|
||||||
|
self.file_path = file_path
|
||||||
|
self.bookid = bookid
|
||||||
|
self.settings = settings
|
||||||
|
self.kindle_mail = kindle_mail
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
self.results = dict()
|
||||||
|
|
||||||
|
def run(self, worker_thread):
|
||||||
|
self.worker_thread = worker_thread
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
cur_book = calibre_db.get_book(self.bookid)
|
||||||
|
data = calibre_db.get_book_format(self.bookid, self.settings['old_book_format'])
|
||||||
|
df = gdriveutils.getFileFromEbooksFolder(cur_book.path,
|
||||||
|
data.name + "." + self.settings['old_book_format'].lower())
|
||||||
|
if df:
|
||||||
|
datafile = os.path.join(config.config_calibre_dir,
|
||||||
|
cur_book.path,
|
||||||
|
data.name + u"." + self.settings['old_book_format'].lower())
|
||||||
|
if not os.path.exists(os.path.join(config.config_calibre_dir, cur_book.path)):
|
||||||
|
os.makedirs(os.path.join(config.config_calibre_dir, cur_book.path))
|
||||||
|
df.GetContentFile(datafile)
|
||||||
|
else:
|
||||||
|
error_message = _(u"%(format)s not found on Google Drive: %(fn)s",
|
||||||
|
format=self.settings['old_book_format'],
|
||||||
|
fn=data.name + "." + self.settings['old_book_format'].lower())
|
||||||
|
return error_message
|
||||||
|
|
||||||
|
filename = self._convert_ebook_format()
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
os.remove(self.file_path + u'.' + self.settings['old_book_format'].lower())
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
# Upload files to gdrive
|
||||||
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
self._handleSuccess()
|
||||||
|
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:
|
||||||
|
worker_thread.add(self.user, TaskEmail(self.settings['subject'], self.results["path"],
|
||||||
|
filename, self.settings, self.kindle_mail,
|
||||||
|
self.settings['subject'], self.settings['body'], internal=True))
|
||||||
|
except Exception as e:
|
||||||
|
return self._handleError(str(e))
|
||||||
|
|
||||||
|
def _convert_ebook_format(self):
|
||||||
|
error_message = None
|
||||||
|
local_session = db.CalibreDB().session
|
||||||
|
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 os.path.basename(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))
|
||||||
|
try:
|
||||||
|
local_session.merge(new_format)
|
||||||
|
local_session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
local_session.rollback()
|
||||||
|
log.error("Database error: %s", e)
|
||||||
|
return
|
||||||
|
self.results['path'] = cur_book.path
|
||||||
|
self.results['title'] = cur_book.title
|
||||||
|
if not config.config_use_google_drive:
|
||||||
|
self._handleSuccess()
|
||||||
|
return os.path.basename(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
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
self.progress *= 0.9
|
||||||
|
|
||||||
|
# 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", error=ele.strip('\n'))
|
||||||
|
return check, error_message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "Convert"
|
@ -0,0 +1,241 @@
|
|||||||
|
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
|
||||||
|
elif hasattr(e, "args"):
|
||||||
|
text = '\n'.join(e.args)
|
||||||
|
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,59 +1,99 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% macro text_table_row(parameter, edit_text, show_text, validate) -%}
|
||||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
<th data-field="{{ parameter }}" id="{{ parameter }}" data-sortable="true"
|
||||||
|
data-visible = "{{visiblility.get(parameter)}}"
|
||||||
<div class="filterheader hidden-xs hidden-sm">
|
{% if g.user.role_edit() %}
|
||||||
{% if entries.__len__() %}
|
data-editable-type="text"
|
||||||
{% if data == 'author' %}
|
data-editable-url="{{ url_for('editbook.edit_list_book', param=parameter)}}"
|
||||||
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
|
data-editable-title="{{ edit_text }}"
|
||||||
{% endif %}
|
data-edit="true"
|
||||||
|
{% if validate %}data-edit-validate="{{ _('This Field is Required') }}" {% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button id="desc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
|
>{{ show_text }}</th>
|
||||||
<button id="asc" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
|
{%- endmacro %}
|
||||||
{% if charlist|length %}
|
|
||||||
<button id="all" class="btn btn-primary">{{_('All')}}</button>
|
|
||||||
{% endif %}
|
|
||||||
<div class="btn-group character" role="group">
|
|
||||||
{% for char in charlist%}
|
|
||||||
<button class="btn btn-primary char">{{char.char}}</button>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if title == "Series" %}
|
{% block header %}
|
||||||
<button class="update-view btn btn-primary" href="#" data-target="series_view" data-view="grid">Grid</button>
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-table.min.css') }}" rel="stylesheet">
|
||||||
{% endif %}
|
<link href="{{ url_for('static', filename='css/libs/bootstrap-editable.css') }}" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h2 class="{{page}}">{{_(title)}}</h2>
|
||||||
|
<div class="col-xs-12 col-sm-6">
|
||||||
|
<div class="row">
|
||||||
|
<div class="btn btn-default disabled" id="merge_books" data-toggle="modal" data-target="#mergeModal" aria-disabled="true">{{_('Merge selected books')}}</div>
|
||||||
|
<div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="col-xs-12 col-sm-6">
|
||||||
<div id="list" class="col-xs-12 col-sm-6">
|
<div class="row">
|
||||||
{% for entry in entries %}
|
<input type="checkbox" id="autoupdate_titlesort" name="autoupdate_titlesort" checked>
|
||||||
{% if loop.index0 == (loop.length/2+loop.length%2)|int and loop.length > 20 %}
|
<label for="autoupdate_titlesort">{{_('Update Title Sort automatically')}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="second" class="col-xs-12 col-sm-6">
|
<div class="row">
|
||||||
|
<input type="checkbox" id="autoupdate_autorsort" name="autoupdate_autorsort" checked>
|
||||||
|
<label for="autoupdate_autorsort">{{_('Update Author Sort automatically')}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="books-table" class="table table-no-bordered table-striped"
|
||||||
|
data-url="{{url_for('web.list_books')}}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% if g.user.role_edit() %}
|
||||||
|
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="row" {% if entry[0].sort %}data-name="{{entry[0].name}}"{% endif %} data-id="{% if entry[0].sort %}{{entry[0].sort}}{% else %}{% if entry.name %}{{entry.name}}{% else %}{{entry[0].name}}{% endif %}{% endif %}">
|
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
|
||||||
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{entry.count}}</span></div>
|
{{ text_table_row('title', _('Enter Title'),_('Title'), true) }}
|
||||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{% if entry.format %}{{url_for('web.books_list', data=data, sort='new', book_id=entry.format )}}{% else %}{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].id )}}{% endif %}">
|
{{ text_table_row('sort', _('Enter Title Sort'),_('Title Sort'), false) }}
|
||||||
{% if entry.name %}
|
{{ text_table_row('author_sort', _('Enter Author Sort'),_('Author Sort'), false) }}
|
||||||
<div class="rating">
|
{{ text_table_row('authors', _('Enter Authors'),_('Authors'), true) }}
|
||||||
{% for number in range(entry.name) %}
|
{{ text_table_row('tags', _('Enter Categories'),_('Categories'), false) }}
|
||||||
<span class="glyphicon glyphicon-star good"></span>
|
{{ text_table_row('series', _('Enter Series'),_('Series'), false) }}
|
||||||
{% if loop.last and loop.index < 5 %}
|
<th data-field="series_index" id="series_index" data-visible="{{visiblility.get('series_index')}}" data-edit-validate="{{ _('This Field is Required') }}" data-sortable="true" {% if g.user.role_edit() %} data-editable-type="number" data-editable-placeholder="1" data-editable-step="0.01" data-editable-min="0" data-editable-url="{{ url_for('editbook.edit_list_book', param='series_index')}}" data-edit="true" data-editable-title="{{_('Enter title')}}"{% endif %}>{{_('Series Index')}}</th>
|
||||||
{% for numer in range(5 - loop.index) %}
|
{{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }}
|
||||||
<span class="glyphicon glyphicon-star"></span>
|
<!--th data-field="pubdate" data-type="date" data-visible="{{visiblility.get('pubdate')}}" data-viewformat="dd.mm.yyyy" id="pubdate" data-sortable="true">{{_('Publishing Date')}}</th-->
|
||||||
{% endfor %}
|
{{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }}
|
||||||
|
{% if g.user.role_edit() %}
|
||||||
|
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
{% block modal %}
|
||||||
|
{{ delete_book(0) }}
|
||||||
|
{% if g.user.role_edit() %}
|
||||||
|
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-center">
|
||||||
|
<span>{{_('Are you really sure?')}}</span>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
<div class="modal-body">
|
||||||
{% if entry.format %}
|
<p></p>
|
||||||
{{entry.format}}
|
<div class="text-left">{{_('Books with Title will be merged from:')}}</div>
|
||||||
{% else %}
|
<p></p>
|
||||||
{{entry[0].name}}{% endif %}{% endif %}</a></div>
|
<div class=text-left" id="merge_from"></div>
|
||||||
|
<p></p>
|
||||||
|
<div class="text-left">{{_('Into Book with Title:')}}</div>
|
||||||
|
<p></p>
|
||||||
|
<div class=text-left" id="merge_to"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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()
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue