diff --git a/addon/UIForm/loginDialog.py b/addon/UIForm/loginDialog.py new file mode 100644 index 0000000..f6c5d4a --- /dev/null +++ b/addon/UIForm/loginDialog.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'designer/loginDialog.ui' +# +# Created by: PyQt5 UI code generator 5.12.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_LoginDialog(object): + def setupUi(self, LoginDialog): + LoginDialog.setObjectName("LoginDialog") + LoginDialog.resize(505, 480) + self.gridLayout = QtWidgets.QGridLayout(LoginDialog) + self.gridLayout.setObjectName("gridLayout") + self.reloadBtn = QtWidgets.QPushButton(LoginDialog) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 0, 1, 1, 1) + self.pageContainer = QtWidgets.QVBoxLayout() + self.pageContainer.setObjectName("pageContainer") + self.gridLayout.addLayout(self.pageContainer, 1, 0, 1, 2) + self.address = QtWidgets.QLineEdit(LoginDialog) + self.address.setClearButtonEnabled(True) + self.address.setObjectName("address") + self.gridLayout.addWidget(self.address, 0, 0, 1, 1) + + self.retranslateUi(LoginDialog) + QtCore.QMetaObject.connectSlotsByName(LoginDialog) + + def retranslateUi(self, LoginDialog): + _translate = QtCore.QCoreApplication.translate + LoginDialog.setWindowTitle(_translate("LoginDialog", "Login")) + self.reloadBtn.setText(_translate("LoginDialog", "reload")) + + diff --git a/addon/UIForm/loginDialog.ui b/addon/UIForm/loginDialog.ui new file mode 100644 index 0000000..2acffb9 --- /dev/null +++ b/addon/UIForm/loginDialog.ui @@ -0,0 +1,38 @@ + + + LoginDialog + + + + 0 + 0 + 505 + 480 + + + + Login + + + + + + reload + + + + + + + + + + true + + + + + + + + diff --git a/addon/addonWindow.py b/addon/addonWindow.py index d26874a..b7f4b49 100644 --- a/addon/addonWindow.py +++ b/addon/addonWindow.py @@ -9,9 +9,10 @@ from .queryApi import apis from .UIForm import wordGroup, mainUI, icons_rc -from .workers import LoginWorker, VersionCheckWorker, RemoteWordFetchingWorker, QueryWorker, AudioDownloadWorker +from .workers import LoginStateCheckWorker, VersionCheckWorker, RemoteWordFetchingWorker, QueryWorker, AudioDownloadWorker from .dictionary import dictionaries from .logger import Handler +from .loginDialog import LoginDialog from .misc import Mask from .constants import BASIC_OPTION, EXTRA_OPTION, MODEL_NAME, RELEASE_URL @@ -130,6 +131,10 @@ def setupGUIByConfig(self): self.selectedGroups = config['selectedGroup'] def initCore(self): + self.usernameLineEdit.hide() + self.usernameLabel.hide() + self.passwordLabel.hide() + self.passwordLineEdit.hide() self.dictionaryComboBox.addItems([d.name for d in dictionaries]) self.apiComboBox.addItems([d.name for d in apis]) self.deckComboBox.addItems(getDeckList()) @@ -198,8 +203,6 @@ def on_dictionaryComboBox_currentIndexChanged(self, index): """词典候选框改变事件""" self.currentDictionaryLabel.setText(f'当前选择词典: {self.dictionaryComboBox.currentText()}') config = mw.addonManager.getConfig(__name__) - self.usernameLineEdit.setText(config['credential'][index]['username']) - self.passwordLineEdit.setText(config['credential'][index]['password']) self.cookieLineEdit.setText(config['credential'][index]['cookie']) @pyqtSlot() @@ -217,25 +220,33 @@ def on_pullRemoteWordsBtn_clicked(self): self.selectedDict = dictionaries[currentConfig['selectedDict']]() # 登陆线程 - self.loginWorker = LoginWorker(self.selectedDict.login, str(currentConfig['username']), str(currentConfig['password']), json.loads(str(currentConfig['cookie']) or '{}')) + self.loginWorker = LoginStateCheckWorker(self.selectedDict.checkCookie, json.loads(self.cookieLineEdit.text() or '{}')) self.loginWorker.moveToThread(self.workerThread) - self.loginWorker.logSuccess.connect(self.onLogSuccess) self.loginWorker.start.connect(self.loginWorker.run) + self.loginWorker.logSuccess.connect(self.onLogSuccess) self.loginWorker.logFailed.connect(self.onLoginFailed) self.loginWorker.start.emit() @pyqtSlot() def onLoginFailed(self): - showCritical('登录失败!') - self.tabWidget.setCurrentIndex(1) + showCritical('第一次登录或cookie失效!请重新登录') self.progressBar.setValue(0) self.progressBar.setMaximum(1) self.mainTab.setEnabled(True) self.cookieLineEdit.clear() + self.loginDialog = LoginDialog( + loginUrl=self.selectedDict.loginUrl, + loginCheckCallbackFn=self.selectedDict.loginCheckCallbackFn, + parent=self + ) + self.loginDialog.loginSucceed.connect(self.onLogSuccess) + self.loginDialog.show() @pyqtSlot(str) def onLogSuccess(self, cookie): self.cookieLineEdit.setText(cookie) + self.getAndSaveCurrentConfig() + self.selectedDict.checkCookie(json.loads(cookie)) self.selectedDict.getGroups() container = QDialog(self) @@ -501,5 +512,3 @@ def on_syncBtn_clicked(self): if not audiosDownloadTasks: tooltip(f'添加{added}个笔记\n删除{deleted}个笔记') - - diff --git a/addon/constants.py b/addon/constants.py index 15d91d0..d307132 100644 --- a/addon/constants.py +++ b/addon/constants.py @@ -1,4 +1,4 @@ -VERSION = 'v6.0.2' +VERSION = 'v6.1.0' RELEASE_URL = 'https://github.com/megachweng/Dict2Anki' VERSION_CHECK_API = 'https://api.github.com/repos/megachweng/Dict2Anki/releases/latest' MODEL_NAME = f'Dict2Anki-{VERSION}' diff --git a/addon/dictionary/eudict.py b/addon/dictionary/eudict.py index 12ae242..ece1ba3 100644 --- a/addon/dictionary/eudict.py +++ b/addon/dictionary/eudict.py @@ -12,6 +12,7 @@ class Eudict(AbstractDictionary): name = '欧陆词典' + loginUrl = 'https://dict.eudic.net/account/login' timeout = 10 headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', @@ -23,22 +24,9 @@ class Eudict(AbstractDictionary): def __init__(self): self.groups = [] + self.indexSoup = None - def login(self, username: str, password: str, cookie: dict = None) -> dict: - """ - 登陆 - :param username: 用户名 - :param password: 密码 - :param cookie: cookie - :return: 登陆成功的cookie - """ - self.session.cookies.clear() - if cookie and self._checkCookie(cookie): - return cookie - else: - return self._login(username, password) - - def _checkCookie(self, cookie: dict) -> bool: + def checkCookie(self, cookie: dict) -> bool: """ cookie有效性检验 :param cookie: @@ -54,32 +42,11 @@ def _checkCookie(self, cookie: dict) -> bool: logger.info('Cookie失效') return False - def _login(self, username: str, password: str) -> dict: - """账号和密码登陆""" - data = { - "UserName": username, - "Password": password, - "returnUrl": "http://my.eudic.net/studylist", - "RememberMe": 'true' - } - try: - rsp = self.session.post( - url='https://dict.eudic.net/Account/Login?returnUrl=https://my.eudic.net/studylist', - timeout=self.timeout, - headers=self.headers, - data=data - ) - cookie = requests.utils.dict_from_cookiejar(self.session.cookies) - if 'EudicWeb' in cookie.keys(): - self.indexSoup = BeautifulSoup(rsp.text, features="html.parser") - logger.info(f'登陆成功') - return cookie - else: - logger.error(f'登陆失败') - return {} - except Exception as error: - logger.exception(f'网络异常:{error}') - return {} + @staticmethod + def loginCheckCallbackFn(cookie, content): + if 'EudicWeb' in cookie: + return True + return False def getGroups(self) -> [(str, int)]: """ diff --git a/addon/dictionary/youdao.py b/addon/dictionary/youdao.py index 28ae78d..7d597fa 100644 --- a/addon/dictionary/youdao.py +++ b/addon/dictionary/youdao.py @@ -6,11 +6,13 @@ from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter from ..misc import AbstractDictionary + logger = logging.getLogger('dict2Anki.dictionary.youdao') class Youdao(AbstractDictionary): name = '有道词典' + loginUrl = 'http://account.youdao.com/login?service=dict&back_url=http://dict.youdao.com/wordbook/wordlist%3Fkeyfrom%3Dnull' timeout = 10 headers = { 'Host': 'dict.youdao.com', @@ -22,23 +24,10 @@ class Youdao(AbstractDictionary): session.mount('https://', HTTPAdapter(max_retries=retries)) def __init__(self): + self.indexSoup = None self.groups = [] - def login(self, username: str, password: str, cookie: dict = None) -> dict: - """ - 登陆 - :param username: 用户名 - :param password: 密码 - :param cookie: cookie - :return: cookie dict - """ - self.session.cookies.clear() - if cookie and self._checkCookie(cookie): - return cookie - else: - return self._login(username, password) - - def _checkCookie(self, cookie) -> bool: + def checkCookie(self, cookie: dict) -> bool: """ cookie有效性检验 :param cookie: @@ -54,37 +43,11 @@ def _checkCookie(self, cookie) -> bool: logger.info('Cookie失效') return False - def _login(self, username: str, password: str) -> dict: - """账号和密码登陆""" - data = (('app', 'mobile'), - ('product', 'DICT'), - ('tp', 'urstoken'), - ('cf', '7'), - ('show', 'true'), - ('format', 'json'), - ('username', username), - ('password', hashlib.md5(password.encode('utf-8')).hexdigest()), - ('um', 'true'),) - try: - self.session.post( - url='https://dict.youdao.com/login/acc/login', - timeout=self.timeout, - headers=self.headers, - data=data - ) - cookie = requests.utils.dict_from_cookiejar(self.session.cookies) - if username and username.lower() in cookie.get('DICT_SESS', ''): - # 登陆后获取单词本首页的soup对象 - rsp = self.session.get('http://dict.youdao.com/wordbook/wordlist', timeout=self.timeout) - self.indexSoup = BeautifulSoup(rsp.text, features="html.parser") - logger.info('登陆成功') - return cookie - else: - logger.error('登陆失败') - return {} - except Exception as error: - logger.exception(f'网络异常:{error}') - return {} + @staticmethod + def loginCheckCallbackFn(cookie, content): + if 'DICT_SESS' in cookie: + return True + return False def getGroups(self) -> [(str, int)]: """ diff --git a/addon/logger.py b/addon/logger.py index c946403..a97d247 100644 --- a/addon/logger.py +++ b/addon/logger.py @@ -11,7 +11,7 @@ def __init__(self, parent): formatter = Formatter('[%(asctime)s][%(levelname)8s] -- %(message)s - (%(name)s)', '%d/%m/%Y %H:%M:%S') self.setFormatter(formatter) - self.setLevel(logging.INFO) + self.setLevel(logging.DEBUG) def emit(self, record): msg = self.format(record) diff --git a/addon/loginDialog.py b/addon/loginDialog.py new file mode 100644 index 0000000..81a4a46 --- /dev/null +++ b/addon/loginDialog.py @@ -0,0 +1,73 @@ +import json +import sys +import logging +from PyQt5.QtCore import QUrl, pyqtSignal +from .UIForm import loginDialog +from PyQt5.QtWidgets import QDialog +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + +logger = logging.getLogger('dict2Anki') + + +class LoginDialog(QDialog, loginDialog.Ui_LoginDialog): + loginSucceed = pyqtSignal(str) + + def __init__(self, loginUrl, loginCheckCallbackFn, parent=None): + super().__init__(parent) + self.url = QUrl(loginUrl) + self.loginCheckCallbackFn = loginCheckCallbackFn + self.setupUi(self) + self.page = LoginWebEngineView(self) + self.pageContainer.addWidget(self.page) + self.page.load(self.url) + self.makeConnection() + + def makeConnection(self): + self.reloadBtn.clicked.connect(self._reload) + self.page.loadFinished.connect(self.checkLoginState) + + def _reload(self): + logger.debug('Reload page') + self.page.cookieStore.deleteAllCookies() + self.page.load(QUrl(self.address.text())) + + def checkLoginState(self): + def contentLoaded(content): + logger.debug(f'Cookie:{self.page.cookie}') + logger.debug(f'Content{content}') + if self.loginCheckCallbackFn(cookie=self.page.cookie, content=content): + logger.info(f'Login Success!') + self.onLoginSucceed() + logger.info(f'Login Fail!') + + self.page.page().toHtml(contentLoaded) + + def onLoginSucceed(self): + logger.info('Destruct login dialog') + self.close() + logger.debug('emit cookie') + self.loginSucceed.emit(json.dumps(self.page.cookie)) + + +class LoginWebEngineView(QWebEngineView): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # 绑定cookie被添加的信号槽 + self.profile = QWebEngineProfile.defaultProfile() + self.profile.setHttpUserAgent( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko)' + ' Chrome/69.0.3497.100 Safari/537.36' + ) + self.cookieStore = self.profile.cookieStore() + self.cookieStore.cookieAdded.connect(self.onCookieAdd) + self._cookies = {} + self.show() + + def onCookieAdd(self, cookie): + name = cookie.name().data().decode('utf-8') + value = cookie.value().data().decode('utf-8') + self._cookies[name] = value + + @property + def cookie(self) -> dict: + return self._cookies diff --git a/addon/misc.py b/addon/misc.py index 7d2c65e..458c117 100644 --- a/addon/misc.py +++ b/addon/misc.py @@ -7,8 +7,14 @@ class AbstractDictionary(ABC): + + @staticmethod + @abstractmethod + def loginCheckCallbackFn(cookie: dict, content: str): + pass + @abstractmethod - def login(self, username: str, password: str, cookie: dict = None) -> dict: + def checkCookie(self, cookie: dict): pass @abstractmethod diff --git a/addon/noteManager.py b/addon/noteManager.py index 0012696..010823c 100644 --- a/addon/noteManager.py +++ b/addon/noteManager.py @@ -45,15 +45,19 @@ def getOrCreateDeck(deckName): def getOrCreateModel(modelName): model = mw.col.models.byName(modelName) if model: - return model - else: - logger.info(f'创建新模版:{modelName}') - newModel = mw.col.models.new(modelName) - mw.col.models.add(newModel) - for field in MODEL_FIELDS: - mw.col.models.addField(newModel, mw.col.models.newField(field)) - mw.col.models.update(newModel) - return newModel + if set([f['name'] for f in model['flds']]) == set(MODEL_FIELDS): + return model + else: + logger.warning('模版字段异常,自动删除重建') + mw.col.models.rem(model) + + logger.info(f'创建新模版:{modelName}') + newModel = mw.col.models.new(modelName) + mw.col.models.add(newModel) + for field in MODEL_FIELDS: + mw.col.models.addField(newModel, mw.col.models.newField(field)) + mw.col.models.update(newModel) + return newModel def getOrCreateModelCardTemplate(modelObject, cardTemplateName): diff --git a/addon/workers.py b/addon/workers.py index 967cbba..11182c5 100644 --- a/addon/workers.py +++ b/addon/workers.py @@ -33,21 +33,20 @@ def run(self): self.finished.emit() -class LoginWorker(QObject): +class LoginStateCheckWorker(QObject): start = pyqtSignal() logSuccess = pyqtSignal(str) logFailed = pyqtSignal() - def __init__(self, LoginFunc, *args, **kwargs): + def __init__(self, checkFn, cookie): super().__init__() - self.LoginFunc = LoginFunc - self.args = args - self.kwargs = kwargs + self.checkFn = checkFn + self.cookie = cookie def run(self): - cookie = self.LoginFunc(*self.args, **self.kwargs) - if cookie: - self.logSuccess.emit(json.dumps(cookie)) + loginState = self.checkFn(self.cookie) + if loginState: + self.logSuccess.emit(json.dumps(self.cookie)) else: self.logFailed.emit()