def _deleteSession(self, filePath):
        result = QMessageBox.information(gVar.app.activeWindow(), _('Delete Session'),
            _('Are you sure you want to delete session \'%s\'?') % QFileInfo(filePath).completeBaseName(),
            QMessageBox.Yes | QMessageBox.No)

        if result == QMessageBox.Yes:
            QFile.remove(filePath)
Example #2
0
 def deleteFile(self, path, name):
     file = QFile(path + name + ".tmp")
     print(file.fileName())
     if file.exists():
         print(file.remove())
     file.setFileName(path + name + ".tmp.cfg")
     if file.exists():
         print(file.remove())
    def backupSavedSessions(self):
        if not QFile.exists(self._lastActiveSessionPath):
            return

        if QFile.exists(self._firstBackupSession):
            QFile.remove(self._secondBackupSession)
            QFile.copy(self._firstBackupSession, self._secondBackupSession)

        QFile.remove(self._firstBackupSession)
        QFile.copy(self._lastActiveSessionPath, self._firstBackupSession)
Example #4
0
    def save (self,filename = None):
        print ("sauvegarde")
        progress = QProgressDialog ()
        progress.setWindowModality(QtCore.Qt.WindowModal)
        progress.setLabelText("Sauvegarde")
        progress.setMaximum(len(self.getWarriorList())+1)
        #db_name = self.database.database.databaseName()
        if filename == None : 
            filename = os.path.join(Config().instance.path_to_sqlite(),self.settings.value("global/current_database"))
        try :
            print ('current filename', filename)
            # backup 
            filename_bkp = filename+"_"+QtCore.QDateTime.currentDateTime().toString("yyyy-MM-dd-hh-mm-ss")
            QFile.copy(filename,filename_bkp)
            if QFile.remove(filename) == False :
                qWarning("echec suppression ")
            else:
                qWarning("reussite suppression %s"% filename)                
        except OSError :
            qWarning("echec suppression ")

        result = QFile.copy(Config().instance.model_database(),filename)
        if result == False :
            print("echec de la copy ",Config().instance.model_database(),filename)
            return
        else:
            print("copy du model reussit")
            database = DatabaseManager(filename,True)
            database.createConnection()
            database.setVerbose(True)
        for faction in self.factions.values() :
            attribs = faction.getDictAttributes ()
            database.insert("gm_faction",attribs)
            for empire in faction.empires.values():
                attribs = empire.getDictAttributes ()
                database.insert("gm_empire",attribs)
                for kingdom in empire.kingdoms.values():
                    attribs = kingdom.getDictAttributes ()
                    database.insert("gm_kingdom",attribs)
                    for temple in kingdom.temples:
                        attribs = temple.getDictAttributes ()
                        database.insert("gm_temple",attribs)                        
                    for groupe in kingdom.groupes.values():
                        attribs = groupe.getDictAttributes ()
                        database.insert("gm_groupe",attribs)
                        for sub_groupe in groupe.sub_groupes:
                            attribs = sub_groupe.getDictAttributes ()
                            database.insert("gm_groupe",attribs)
                            for perso in sub_groupe.warriors.values():
                                attribs = perso.getDictAttributes ()
                                database.insert("gm_perso",attribs)
                        for perso in groupe.warriors.values():
                            attribs = perso.getDictAttributes ()
                            database.insert("gm_perso",attribs)
                            progress.setValue(progress.value()+1)
Example #5
0
 def removeRecursively(self, filePath):
     '''
     @param: filePath QString
     '''
     fileInfo = QFileInfo(filePath)
     if not fileInfo.exists() and not fileInfo.isSymLink():
         return
     if fileInfo.isDir() and not fileInfo.isSymLink():
         dir_ = QDir(filePath)
         dir_ = dir_.canonicalPath()
         if dir_.isRoot() or dir_.path() == QDir.home().canonicalPath():
             print('CRITICAL: Attempt to remove root/home directory', dir_)
             return False
         fileNames = dir_.entryList(QDir.Files | QDir.Dirs | QDir.NoDotAndDotDot |
                 QDir.Hidden | QDir.System)
         for fileName in fileNames:
             if not self.removeRecursively(filePath + '/' + fileName):
                 return False
         if not QDir.root().rmdir(dir_.path()):
             return False
     elif not QFile.remove(filePath):
         return False
     return True
Example #6
0
class HttpsDownloader(QObject):
    # pyqtSignal
    downloadProgress = pyqtSignal("qint64",
                                  "qint64",
                                  "qint64",
                                  arguments=["receiver", "total", "timeStamp"])

    # 初始化函数
    def __init__(self, parent=None):
        super(HttpsDownloader, self).__init__(parent)

        self._manager = QNetworkAccessManager(self)
        self._attributes = DownloaderAttributes(
            DownloaderAttributes.UrlType.Https, self)
        self._tmpFile = QFile(self)
        self._configFile = QFile(self)
        self._reply = None

    # 析构函数
    def __del__(self):
        # 这些析构函数会报错
        # if self._tmpFile.isOpen():
        #     self._tmpFile.close()
        # if self._configFile.isOpen():
        #     self._configFile.close()
        # if self._reply:
        #     self._reply.deleteLater()
        pass

    # 类的属性,提供给qml使用# # # # # # # # # # # # # # # # # # # # # #
    # # # # # # # # # # # # # # # # # # # # attributes# # # # # # # # #
    @pyqtProperty(DownloaderAttributes)
    def attributes(self):
        return self._attributes

    # 判断文件是否在此目录
    # path:文件所在路径
    # filename:文件名(不带路径,带后缀名)
    def isFileExist(self, path, filename):
        dir = QDir(path)
        return filename in dir.entryList()

    # 任务状态的改变,用于用户操作或者出现异常情况时设置
    # 通过UI通知用户,不弹窗
    def changeState(self, state):
        self.attributes.setState(state)

    # 这里判断目录是否存在,不存在则创建
    def isDirExist(self, fullPath):
        dir = QDir(fullPath)
        if dir.exists():
            return True
        else:
            return dir.mkpath(fullPath)

    # 打开配置文件
    def _openConfig(self):
        config = self.attributes.fileName + '.tmp.cfg'
        self._configFile.setFileName(self.attributes.path + config)
        self._configFile.open(QFile.ReadWrite)

    # 打开临时文件,用于保存下载信息
    # 在这里重置文件大小
    def _openTemp(self):
        tempname = self.attributes.fileName + '.tmp'
        if self._tmpFile.isOpen():
            self._tmpFile.close()
        self._tmpFile.setFileName(self.attributes.path + tempname)
        self._tmpFile.open(QFile.Append)
        self._tmpFile.seek(self.attributes.preProgress)

    # 保存config
    def _saveConfig(self):
        if not self._configFile.isOpen():
            return False
        info = self.attributes.toJson()
        self._configFile.resize(0)
        self._configFile.seek(0)
        self._configFile.write(QJsonDocument(info).toJson())
        self._configFile.flush()
        return True

    # 读取config
    def _loadConfig(self):
        self._configFile.seek(0)
        cfg = self._configFile.readAll()
        if len(cfg) <= 0:
            return False
        j = QJsonDocument.fromJson(cfg).object()
        self.attributes.fromJson(j)
        return True

    # 开始下载文件
    def startDownload(self, isPause):
        # 这里区分批量下载和单个文件下载
        if self.attributes.totalFile == 1:
            # 这里不需要重新计算文件名,因为在UI已经确认过了,这里如果文件名相同则会是
            # 继续下载,所以不存在会文件名相同
            # 单个文件下载则任务名和文件名同名
            self.attributes.taskName = self.attributes.fileName
            # 打开配置文件并保存,这一步放在设置文件名后执行
            self._openConfig()
            if self._loadConfig():
                # 存在历史纪录
                pass
            else:
                self._saveConfig()
            self._openTemp()
            if not isPause:
                # 如果不是暂停,则下载
                self._download_signal(self.attributes.url)
            else:
                self.changeState(DownloaderAttributes.States.pause)
        else:
            self.attributes.path = self.attributes.path[:-1] + self.attributes.fileName
            # 生成目录
            self.isDirExist(self.attributes.path)
            # 重置文件名,用于生成配置文件而已
            if len(self.attributes.fileName.split('/')) >= 2:
                self.attributes.fileName = self.attributes.checkFileName(
                    self.attributes.fileName.split('/')[-2],
                    self.attributes.path)
            else:
                self.attributes.fileName = self.attributes.checkFileName(
                    self.attributes.fileName, self.attributes.path)
            # 多个文件下载则是文件夹的名字
            self.attributes.taskName = self.attributes.fileName
            # 打开配置文件并保存,这一步放在设置文件名后执行
            self._openConfig()
            if self._loadConfig():
                # 存在历史纪录
                pass
            else:
                self._saveConfig()
            if not isPause:
                # 如果不是暂停,则下载
                # 设置为另一个接口主要的意图在于,多个文件下载完都会调用一次该接口
                self._download_mult()
            else:
                self.changeState(DownloaderAttributes.States.pause)

    # 下载单个文件
    # total用来表示文件大小,这样的优点在于预加载就可以显示文件大小的信息
    def _download_signal(self, url):

        request = QNetworkRequest()
        request.setRawHeader(
            str("Range").encode(),
            str("bytes=" + str(self.attributes.preProgress) + "-").encode())
        # 设置自动重定向,之前自己实现的链接跳转就可以去掉了
        request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        request.setUrl(QUrl(url))

        self.changeState(DownloaderAttributes.States.downloading)
        self._reply = self._manager.get(request)
        self._reply.downloadProgress.connect(self.writeFile)
        self._reply.finished.connect(self.downloadError)
        return True

    # 下载多个文件
    def _download_mult(self):
        # 任务开始前先重置下载进度
        name = self.attributes.url.split('/')[-1].split('.')
        newNum = str(int(name[0]) + self.attributes.finishFile).zfill(
            len(name[0]))
        newNum = newNum + "." + name[1]
        self.attributes.fileName = newNum
        url = self.attributes.url[:self.attributes.url.rfind('/') + 1] + newNum
        self._openTemp()
        print(url)
        self._download_signal(url)
        pass

    # 把数据写入文件的指令
    def _writeFile(self, data):
        self._tmpFile.seek(self.attributes.curProgress)
        b = self._tmpFile.writeData(data)
        return b

    # 槽函数
    # 用于任务终止时输出错误信息,下载完成也会触发
    @pyqtSlot()
    def downloadError(self):
        print("IOError", self._reply.errorString())
        print("NetworkError", self._reply.error())
        logging.info("IOError" + self._reply.errorString())

    # 槽函数
    # 把下载的内容写入文件
    # receive:已接收的字节数
    # total:文件总大小
    @pyqtSlot("qint64", "qint64")
    def writeFile(self, receive, total):
        # print("receive",receive)
        # print("total",total)
        # print(self._reply.error())

        if total <= 0:
            # -1有可能是下载错误
            if receive <= 0:
                self.changeState(DownloaderAttributes.States.networkError)
                # 这里不打算中断网络链接,经过观察再决定这里是否中断
                return
            else:
                # 这种情况是total返回-1,但是实际上是有下载的
                self.changeState(DownloaderAttributes.States.totalLessThanZero)

        # 这里是刚开始下载的时候,total默认是0,设置下载的文件大小
        if self.attributes.total != total:
            self.attributes.total = total + self.attributes.preProgress
            if self._tmpFile.isOpen():
                if self._tmpFile.size() < total:
                    self._tmpFile.resize(total)
            else:
                self.changeState(DownloaderAttributes.States.fileOpenError)
                return

        # 这里是暂停续传部分,因为更改了暂停机制,在暂停前取消信号连接,
        # 所以这里基本不存在触发的情况
        if self.attributes._state == DownloaderAttributes.States.pause:
            # 暂停触发,不处理后面接受到的字节数
            if self._reply.isRunning():
                # 如果reply还没有关闭则关闭下载通道
                # 这个在软件重开续传和预读头文件的时候预防下载通道还没关闭的情况
                self._reply.downloadProgress.disconnect(self.writeFile)
                self._reply.abort()
            return
        elif not self._reply.isOpen():
            if self.attributes.curProgress == self.attributes.total:
                # 其实这里已经排除abort触发的情况因为在abort之前已经断开信号连接
                # 这里的条件判断以及输出是用于日志输出
                print("abort 触发")
                pass
            else:
                # 如果不是因为暂停而关闭reply的通道的情况则需要通报用户下载失败
                self.changeState(DownloaderAttributes.States.networkError)
                return
        else:
            #  正常情况
            data = self._reply.readAll()
            if len(data) == 0:
                #  没有数据读取
                if self._reply.error() == QNetworkReply.NoError:
                    self.success()
                else:
                    self.changeState(DownloaderAttributes.States.networkError)
                return
            if self._tmpFile.writeData(data) <= 0:
                self.changeState(DownloaderAttributes.States.fileWriteError)
                return

        # 获取时间戳,因为我不知道qml如何获取,然后发射信号给qml更新界面
        self._tmpFile.flush()
        self.attributes.curTime = int(time.time())
        self.attributes.curProgress = receive + self.attributes.preProgress
        self._saveConfig()
        # 下载完成的处理,不使用finish是因为那个信号啥都会触发
        if self.attributes.curProgress == self.attributes.total:
            self.success()

    # 槽函数
    # 暂停/继续下载
    @pyqtSlot(bool)
    def pauseDown(self):
        if self.attributes._state == DownloaderAttributes.States.downloading:
            # 正在下载,应该处理暂停
            self.changeState(DownloaderAttributes.States.pause)
            if self._reply != None and self._reply.isRunning():
                self._reply.downloadProgress.disconnect(self.writeFile)
                self._reply.abort()
            self.attributes.preProgress = self.attributes.curProgress
        else:
            self.changeState(DownloaderAttributes.States.downloading)
            if self.attributes.totalFile > 1:
                self._download_mult()
            else:
                self._download_signal(self.attributes.url)

    # 槽函数
    # 下载成功,设置标志位finish为true,然后停止下载.
    @pyqtSlot()
    def success(self):
        self.attributes.completedOne()
        # 虽然下载完成,但是还是需要关闭通道
        # 这里解决了只能下载第一个文件的情况,也解决了下载完成所有任务后,多下载两个文件
        # 貌似是下载文件时多次触发success,所以要断开信号和关闭下载通道
        self._reply.downloadProgress.disconnect(self.writeFile)
        self._reply.abort()
        name = self.attributes.checkFileName(self.attributes.fileName,
                                             self.attributes.path)
        self._tmpFile.rename(self.attributes.path + name)
        self._tmpFile.close()
        # 下载完所有文件,同样适用于下载单个文件
        if self.attributes.finishFile == self.attributes.totalFile:
            pass
        else:
            # 这个情况只有下载多个文件的时候才会出现
            # 重置多个文件下载时的参数
            self.attributes.downloadParamReset()
            self._saveConfig()
            self._download_mult()
            return
        self._configFile.remove()
        self.changeState(DownloaderAttributes.States.finish)

    # 删除这次任务,当然,这里只是关闭文件而已
    # 正真删除操作在setting类
    # 因为只有正在下载的任务有这个类
    # 重构后,可能在这里实现文件的删除操作
    def deleteFile(self):
        self._tmpFile.close()
        self._configFile.close()
Example #7
0
class UploadWorker(QObject):
    readyForNext = pyqtSignal(int)
    uploadProgress = pyqtSignal(int, int, int)
    uploaded = pyqtSignal(int)
    updated = pyqtSignal(int)
    skipped = pyqtSignal(int)
    failed = pyqtSignal(int, str)
    aborted = pyqtSignal(str)

    def __init__(self, index, reupload, db, logger):
        QObject.__init__(self)

        self.index = index
        self.reupload = reupload
        self.db = db
        self.logger = logger
        self.api_key = prefs['api_key']
        self.reply = None
        self.canceled = False

        self.retries = 0

    def start(self):
        self.network = QNetworkAccessManager()
        self.network.authenticationRequired.connect(self.auth)

        self.readyForNext.emit(self.index)

    def cancel(self):
        self.canceled = True
        if self.reply:
            self.reply.abort()

    def sync(self, book_id, file_path):
        self.book_id = book_id
        self.file_path = file_path

        self.check()

    def auth(self, reply, authenticator):
        if not authenticator.user():
            authenticator.setUser(self.api_key)
            authenticator.setPassword('')

    def check(self):
        self.digest = None

        identifiers = self.db.get_proxy_metadata(self.book_id).identifiers
        if identifiers.get('bookfusion'):
            self.is_search_req = False
            self.req = api.build_request('/uploads/' +
                                         identifiers['bookfusion'])
            self.log_info('Upload check: bookfusion={}'.format(
                identifiers['bookfusion']))
        elif identifiers.get('isbn'):
            self.is_search_req = True
            self.req = api.build_request('/uploads',
                                         {'isbn': identifiers['isbn']})
            self.log_info('Upload check: isbn={}'.format(identifiers['isbn']))
        else:
            self.calculate_digest()

            self.is_search_req = False
            self.req = api.build_request('/uploads/' + self.digest)
            self.log_info('Upload check: digest={}'.format(self.digest))

        self.reply = self.network.get(self.req)
        self.reply.finished.connect(self.complete_check)

    def complete_check(self):
        abort = False
        skip = False
        update = False
        result = None

        error = self.reply.error()
        if error == QNetworkReply.AuthenticationRequiredError:
            abort = True
            self.aborted.emit('Invalid API key.')
            self.log_info('Upload check: AuthenticationRequiredError')
        elif error == QNetworkReply.NoError:
            resp = self.reply.readAll()
            self.log_info('Upload check response: {}'.format(resp))

            if self.is_search_req:
                results = json.loads(resp.data())
                if len(results) > 0:
                    result = results[0]
            else:
                result = json.loads(resp.data())

            if result is not None:
                self.set_bookfusion_id(result['id'])
                update = True
        elif error == QNetworkReply.ContentNotFoundError:
            self.log_info('Upload check: ContentNotFoundError')
        elif error == QNetworkReply.InternalServerError:
            self.log_info('Upload check: InternalServerError')
            resp = self.reply.readAll()
            self.log_info('Upload check response: {}'.format(resp))
        elif error == QNetworkReply.UnknownServerError:
            self.log_info('Upload check: UnknownServerError')
            resp = self.reply.readAll()
            self.log_info('Upload check response: {}'.format(resp))
        elif error == QNetworkReply.OperationCanceledError:
            abort = True
            self.log_info('Upload check: OperationCanceledError')
        else:
            abort = True
            self.aborted.emit('Error {}.'.format(error))
            self.log_info('Upload check error: {}'.format(error))

        self.reply.deleteLater()
        self.reply = None

        if not abort:
            if skip:
                self.readyForNext.emit(self.index)
            else:
                self.metadata_digest = self.get_metadata_digest()
                if not result is None and self.metadata_digest == result[
                        'calibre_metadata_digest'] and not self.reupload:
                    self.skipped.emit(self.book_id)
                    self.readyForNext.emit(self.index)
                else:
                    if update:
                        self.update()
                    else:
                        self.init_upload()

    def init_upload(self):
        self.calculate_digest()

        self.req = api.build_request('/uploads/init')
        self.req_body = QHttpMultiPart(QHttpMultiPart.FormDataType)
        self.req_body.append(
            self.build_req_part('filename', path.basename(self.file_path)))
        self.req_body.append(self.build_req_part('digest', self.digest))

        self.reply = self.network.post(self.req, self.req_body)
        self.reply.finished.connect(self.complete_init_upload)

    def complete_init_upload(self):
        resp, retry, abort = self.complete_req('Upload init', return_json=True)

        if retry:
            self.init_upload()
            return

        if abort:
            return

        if resp is not None:
            self.upload_url = resp['url']
            self.upload_params = resp['params']
            self.upload()
        else:
            self.readyForNext.emit(self.index)

    def upload(self):
        self.file = QFile(self.file_path)
        self.file.open(QIODevice.ReadOnly)

        self.req = QNetworkRequest(QUrl(self.upload_url))

        self.req_body = QHttpMultiPart(QHttpMultiPart.FormDataType)
        for key, value in self.upload_params.items():
            self.log_info('{}={}'.format(key, value))
            self.req_body.append(self.build_req_part(key, value))
        self.req_body.append(self.build_req_part('file', self.file))

        self.reply = self.network.post(self.req, self.req_body)
        self.reply.finished.connect(self.complete_upload)
        self.reply.uploadProgress.connect(self.upload_progress)

    def complete_upload(self):
        if self.file:
            self.file.close()

        resp, retry, abort = self.complete_req('Upload')

        if retry:
            self.upload()
            return

        if abort:
            return

        if resp is not None:
            self.finalize_upload()
        else:
            self.readyForNext.emit(self.index)

    def finalize_upload(self):
        self.req = api.build_request('/uploads/finalize')

        self.req_body = QHttpMultiPart(QHttpMultiPart.FormDataType)
        self.req_body.append(
            self.build_req_part('key', self.upload_params['key']))
        self.req_body.append(self.build_req_part('digest', self.digest))
        self.append_metadata_req_parts()

        self.reply = self.network.post(self.req, self.req_body)
        self.reply.finished.connect(self.complete_finalize_upload)

    def complete_finalize_upload(self):
        self.clean_metadata_req()

        resp, retry, abort = self.complete_req('Upload finalize',
                                               return_json=True)

        if retry:
            self.finalize_upload()
            return

        if abort:
            return

        if resp is not None:
            self.set_bookfusion_id(resp['id'])
            self.uploaded.emit(self.book_id)

        self.readyForNext.emit(self.index)

    def update(self):
        if not prefs['update_metadata'] and not self.reupload:
            self.skipped.emit(self.book_id)
            self.readyForNext.emit(self.index)
            return

        identifiers = self.db.get_proxy_metadata(self.book_id).identifiers
        if not identifiers.get('bookfusion') and not self.reupload:
            self.skipped.emit(self.book_id)
            self.readyForNext.emit(self.index)
            return

        self.req = api.build_request('/uploads/' + identifiers['bookfusion'])
        self.req_body = QHttpMultiPart(QHttpMultiPart.FormDataType)

        if self.reupload:
            self.file = QFile(self.file_path)
            self.file.open(QIODevice.ReadOnly)
            self.req_body.append(self.build_req_part('file', self.file))

        self.append_metadata_req_parts()

        self.reply = self.network.put(self.req, self.req_body)
        self.reply.finished.connect(self.complete_update)

    def complete_update(self):
        self.clean_metadata_req()

        resp, retry, abort = self.complete_req('Update')

        if retry:
            self.update()
            return

        if abort:
            return

        if resp is not None:
            self.updated.emit(self.book_id)

        self.readyForNext.emit(self.index)

    def upload_progress(self, sent, total):
        self.uploadProgress.emit(self.book_id, sent, total)

    def log_info(self, msg):
        self.logger.info('[worker-{}] {}'.format(self.index, msg))

    def get_metadata_digest(self):
        metadata = self.db.get_proxy_metadata(self.book_id)

        h = sha256()

        language = next(iter(metadata.languages), None)
        summary = metadata.comments
        isbn = metadata.isbn
        issued_on = metadata.pubdate.date().isoformat()
        if issued_on == '0101-01-01':
            issued_on = None

        h.update(metadata.title.encode('utf-8'))
        if summary:
            h.update(summary.encode('utf-8'))
        if language:
            h.update(language.encode('utf-8'))
        if isbn:
            h.update(isbn.encode('utf-8'))
        if issued_on:
            h.update(issued_on.encode('utf-8'))

        for series_item in self.get_series(metadata):
            h.update(series_item['title'].encode('utf-8'))
            if series_item['index'] is not None:
                h.update(str(series_item['index']).encode('utf-8'))

        for author in metadata.authors:
            h.update(author.encode('utf-8'))
        for tag in metadata.tags:
            h.update(tag.encode('utf-8'))

        bookshelves = self.get_bookshelves(metadata)
        if bookshelves is not None:
            for bookshelf in bookshelves:
                h.update(bookshelf.encode('utf-8'))

        cover_path = self.db.cover(self.book_id, as_path=True)
        if cover_path:
            h.update(bytes(path.getsize(cover_path)))
            h.update(b'\0')
            with open(cover_path, 'rb') as file:
                block = file.read(65536)
                while len(block) > 0:
                    h.update(block)
                    block = file.read(65536)

        return h.hexdigest()

    def append_metadata_req_parts(self):
        metadata = self.db.get_proxy_metadata(self.book_id)
        language = next(iter(metadata.languages), None)
        summary = metadata.comments
        isbn = metadata.isbn
        issued_on = metadata.pubdate.date().isoformat()
        if issued_on == '0101-01-01':
            issued_on = None

        self.req_body.append(
            self.build_req_part('metadata[calibre_metadata_digest]',
                                self.metadata_digest))
        self.req_body.append(
            self.build_req_part('metadata[title]', metadata.title))
        if summary:
            self.req_body.append(
                self.build_req_part('metadata[summary]', summary))
        if language:
            self.req_body.append(
                self.build_req_part('metadata[language]', language))
        if isbn:
            self.req_body.append(self.build_req_part('metadata[isbn]', isbn))
        if issued_on:
            self.req_body.append(
                self.build_req_part('metadata[issued_on]', issued_on))

        for series_item in self.get_series(metadata):
            self.req_body.append(
                self.build_req_part('metadata[series][][title]',
                                    series_item['title']))
            if series_item['index'] is not None:
                self.req_body.append(
                    self.build_req_part('metadata[series][][index]',
                                        str(series_item['index'])))

        for author in metadata.authors:
            self.req_body.append(
                self.build_req_part('metadata[author_list][]', author))
        for tag in metadata.tags:
            self.req_body.append(
                self.build_req_part('metadata[tag_list][]', tag))

        bookshelves = self.get_bookshelves(metadata)
        if bookshelves is not None:
            self.req_body.append(
                self.build_req_part('metadata[bookshelves][]', ''))
            for bookshelf in bookshelves:
                self.req_body.append(
                    self.build_req_part('metadata[bookshelves][]', bookshelf))

        cover_path = self.db.cover(self.book_id, as_path=True)
        if cover_path:
            self.cover = QFile(cover_path)
            self.cover.open(QIODevice.ReadOnly)
            self.req_body.append(
                self.build_req_part('metadata[cover]', self.cover))
        else:
            self.cover = None

    def clean_metadata_req(self):
        if self.cover:
            self.cover.remove()

    def build_req_part(self, name, value):
        part = QHttpPart()
        part.setHeader(QNetworkRequest.ContentTypeHeader, None)
        if isinstance(value, QFile):
            filename = QFileInfo(value).fileName()
            part.setHeader(
                QNetworkRequest.ContentDispositionHeader,
                'form-data; name="{}"; filename="{}"'.format(
                    self.escape_quotes(name), self.escape_quotes(filename)))
            part.setBodyDevice(value)
        else:
            part.setHeader(
                QNetworkRequest.ContentDispositionHeader,
                'form-data; name="{}"'.format(self.escape_quotes(name)))
            part.setBody(value.encode('utf-8'))
        return part

    def complete_req(self, tag, return_json=False):
        retry = False
        abort = False

        if self.canceled:
            abort = True

        error = self.reply.error()
        resp = None
        if error == QNetworkReply.AuthenticationRequiredError:
            abort = True
            self.aborted.emit('Invalid API key.')
            self.log_info('{}: AuthenticationRequiredError'.format(tag))
        elif error == QNetworkReply.NoError:
            resp = self.reply.readAll()
            self.log_info('{} response: {}'.format(tag, resp))
            if return_json:
                try:
                    resp = json.loads(resp.data())
                except ValueError as e:
                    resp = None
                    self.log_info('{}: {}'.format(tag, e))
                    self.failed.emit(self.book_id,
                                     'Cannot parse the server response')
        elif error == QNetworkReply.UnknownContentError:
            if self.reply.attribute(
                    QNetworkRequest.HttpStatusCodeAttribute) == 422:
                err_resp = self.reply.readAll()
                self.log_info('{} response: {}'.format(tag, err_resp))
                msg = json.loads(err_resp.data())['error']
                self.failed.emit(self.book_id, msg)
            else:
                self.log_info('{}: UnknownContentError'.format(tag))
        elif error == QNetworkReply.InternalServerError:
            self.log_info('{}: InternalServerError'.format(tag))
            err_resp = self.reply.readAll()
            self.log_info('{} response: {}'.format(tag, err_resp))
        elif error == QNetworkReply.UnknownServerError:
            self.log_info('{}: UnknownServerError'.format(tag))
            err_resp = self.reply.readAll()
            self.log_info('{} response: {}'.format(tag, err_resp))
        elif error == QNetworkReply.ConnectionRefusedError or \
             error == QNetworkReply.RemoteHostClosedError or \
             error == QNetworkReply.HostNotFoundError or \
             error == QNetworkReply.TimeoutError or \
             error == QNetworkReply.TemporaryNetworkFailureError:
            retry = True
            self.log_info('{}: {}'.format(tag, error))
        elif error == QNetworkReply.OperationCanceledError:
            abort = True
            self.log_info('{}: OperationCanceledError'.format(tag))
        else:
            abort = True
            self.aborted.emit('Error {}.'.format(error))
            self.log_info('{} error: {}'.format(tag, error))

        self.reply.deleteLater()
        self.reply = None

        if retry:
            self.retries += 1

            if self.retries > 2:
                self.retries = 0
                self.aborted.emit('Error {}.'.format(error))
                retry = False
            else:
                abort = False
        else:
            self.retries = 0

        return (resp, retry, abort)

    def calculate_digest(self):
        if self.digest is not None:
            return

        h = sha256()
        h.update(bytes(path.getsize(self.file_path)))
        h.update(b'\0')
        with open(self.file_path, 'rb') as file:
            block = file.read(65536)
            while len(block) > 0:
                h.update(block)
                block = file.read(65536)
        self.digest = h.hexdigest()

    def escape_quotes(self, value):
        return value.replace('"', '\\"')

    def set_bookfusion_id(self, bookfusion_id):
        identifiers = self.db.get_proxy_metadata(self.book_id).identifiers
        identifiers['bookfusion'] = str(bookfusion_id)
        self.db.set_field('identifiers', {self.book_id: identifiers})

    def get_bookshelves(self, metadata):
        bookshelves_custom_column = prefs['bookshelves_custom_column']
        if bookshelves_custom_column:
            try:
                bookshelves = getattr(metadata, bookshelves_custom_column)
            except AttributeError:
                return None
            if bookshelves is None:
                return []
            if isinstance(bookshelves, list):
                return bookshelves
            else:
                return [bookshelves]
        else:
            return None

    def get_series(self, metadata):
        series_items = []
        if metadata.series:
            series_items.append({
                'title': metadata.series,
                'index': metadata.series_index
            })

        for key, meta in self.db.field_metadata.custom_iteritems():
            if meta['datatype'] == 'series':
                title = getattr(metadata, key)
                if title:
                    found = False
                    for series_item in series_items:
                        if series_item['title'].lower() == title.lower():
                            found = True
                    if not found:
                        index = getattr(metadata, key + '_index')
                        series_items.append({'title': title, 'index': index})

        return series_items