From 55c917d17f91f4bba386f6f3cbf63a21a0f66307 Mon Sep 17 00:00:00 2001 From: luolongfei Date: Fri, 24 Dec 2021 11:39:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BC=82=E5=B8=B8=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=9B=E5=AE=8C=E5=96=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=9B=E4=BC=98=E5=8C=96=20docker-compose.yml=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=A1=B9=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 69 ++++++++++++++++-------- docker-compose.yml | 2 + netflix.py | 95 ++++++++++++++++++++++++++------- resources/Netflix_Logo_RGB.png | Bin 0 -> 16470 bytes 4 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 resources/Netflix_Logo_RGB.png diff --git a/README.md b/README.md index 828eb07..17968f0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,31 @@
+ +[![Netflix.png](https://s4.ax1x.com/2021/12/24/TYGHXD.png)](https://s4.ax1x.com/2021/12/24/TYGHXD.png) +

Netflix

监听奈飞(Netflix)密码变更邮件,自动重置密码。
-### 简介 +### 缘起 + +共享 Netflix 账户的用户,密码可能频繁被人修改,使大家无法登录。 -共享 Netflix 账户的用户,最大的烦恼莫过于密码频繁被不良人修改,本项目完美解决了这个问题。基本逻辑是监听 Netflix 密码变更邮件,自动重置密码。 -仅供 Netflix 账户主使用。 +本项目完美解决了这个问题,基本逻辑是监听 Netflix 密码变更邮件,自动重置密码。仅供 Netflix 账户主使用。 ### 使用方法 -*这里只说明如何在 docker 中使用,按照步骤走即可。* +*这里只说明如何在 Docker 中使用,按照步骤走即可。* -#### 1、安装 docker +#### 1、安装 Docker 升级源并安装软件(下面两行命令二选一,根据你自己的系统) -Debian / Ubuntu - -```shell -apt-get update && apt-get install -y wget vim git -``` - -CentOS - ```shell -yum update && yum install -y wget vim git +apt-get update && apt-get install -y wget vim git # Debian / Ubuntu +yum update && yum install -y wget vim git # CentOS ``` -一句话命令安装 docker +一句话命令安装 Docker ```shell wget -qO- get.docker.com | bash @@ -37,7 +34,7 @@ wget -qO- get.docker.com | bash 说明:请使用 KVM 架构的 VPS,OpenVZ 架构的 VPS 不支持安装 Docker,另外 CentOS 8 不支持用此脚本来安装 Docker。 更多关于 Docker 安装的内容参考 [Docker 官方安装指南](https://docs.docker.com/engine/install/) 。 -启动 docker +启动 Docker ```shell systemctl start docker @@ -50,9 +47,9 @@ sudo systemctl enable docker.service sudo systemctl enable containerd.service ``` -#### 2、安装 docker-compose +#### 2、安装 Docker-compose -一句话命令安装 docker-compose,如果想自定义版本,可以修改下面的版本号(`DOCKER_COMPOSE_VER`对应的值),否则保持默认就好。 +一句话命令安装 Docker-compose,如果想自定义版本,可以修改下面的版本号(`DOCKER_COMPOSE_VER`对应的值),否则保持默认就好。 ```shell DOCKER_COMPOSE_VER=1.29.2 && sudo curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VER}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose && sudo ln -snf /usr/local/bin/docker-compose /usr/bin/docker-compose && docker-compose --version @@ -67,15 +64,45 @@ git clone https://github.com/luolongfei/netflix.git && cd netflix #### 4、修改 .env 配置 完成步骤 3 后,现在你应该正位于源码根目录,即 `.env.example` 文件所在目录,执行 + ```shell cp .env.example .env ``` -然后使用`vim`修改`.env`文件中的配置项。注意在 docker 中运行的话,`DRIVER_EXECUTABLE_FILE`、`REDIS_HOST`以及`REDIS_PORT`的值保持默认即可。 + +然后使用`vim`修改`.env`文件中的配置项。注意在 Docker 中运行的话,`DRIVER_EXECUTABLE_FILE`、`REDIS_HOST`以及`REDIS_PORT`的值保持默认即可。 #### 5、运行 直接执行 + ```shell -docker-compose up -d +docker-compose up -d --build ``` -执行完成后,项目便在后台跑起来了,再执行 `docker-compose ps` 可以看到程式的运行状态。 \ No newline at end of file + +执行完成后,项目便在后台跑起来了。 + +#### 6、Docker-compose 常用命令 + +查看程式的运行状态 + +```shell +docker-compose ps +``` +输出程序日志 +```shell +docker-compose logs +``` + +更多 Docker-compose 命令请参考: [Docker-compose 官方指南](https://docs.docker.com/compose/reference/) 。在官网能找到所有命令。 + +#### 7、问答 + +> 如何升级到新版本呢? +> +请在`docker-compose.yml`文件所在目录,拉取最新的代码,然后同样执行`docker-compose up -d --build`,Docker 会自动使用最新的代码进行构建, +构建完跑起来后,即是最新版本。 + +> 非 Netflix 账户主可以使用本项目吗? +> +不能。本项目仅供 Netflix 账户主使用,因为涉及到监听 Netflix 账户的邮件,而只有 Netflix 账户主才有 Netflix 邮箱以及其密码的权限,所以只有 Netflix +账户主有权使用。 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7753ac0..9ed0cb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: build: context: . dockerfile: Dockerfile + depends_on: + - redis container_name: netflix volumes: - .:/conf diff --git a/netflix.py b/netflix.py index b3cc8eb..ae6bd45 100644 --- a/netflix.py +++ b/netflix.py @@ -77,7 +77,7 @@ def wrapper(*args, **kwargs): class Netflix(object): - VERSION = 'v0.4' + VERSION = 'v0.5.1' # 超时秒数,包括隐式等待和显式等待 TIMEOUT = 23 @@ -302,9 +302,7 @@ def __forgot_password(self, netflix_username: str): time.sleep(2) - self.click_forgot_pwd_btn() - - self.handle_unknown_error_alert(self.click_forgot_pwd_btn, 12) + self.handle_click_events(self.click_forgot_pwd_btn, max_num_of_attempts=12) # 直到页面显示已发送邮件 logger.debug('检测是否已到送信完成画面') @@ -355,9 +353,7 @@ def __reset_password(self, curr_netflix_password: str, new_netflix_password: str time.sleep(1) - self.click_submit_btn() - - self.handle_unknown_error_alert(self.click_submit_btn) + self.handle_click_events(self.click_submit_btn) return self.__pwd_change_result() except Exception as e: @@ -390,17 +386,57 @@ def click_submit_btn(self): """ self.driver.find_element_by_id('btn-save').click() - def element_visibility_of(self, xpath: str) -> WebElement or None: + def element_visibility_of(self, xpath: str, verify_val: bool = False, + max_num_of_attempts: int = 3, el_wait_time: int = 2) -> WebElement or None: """ 元素是否存在且可见 适用于在已经加载完的网页做检测,可见且存在则返回元素,否则返回 None :param xpath: + :param verify_val: 如果传入 True,则验证元素是否有值,或者 inner HTML 不为空,并作为关联条件 + :param max_num_of_attempts: 最大尝试次数,由于有的元素的值可能是异步加载的,需要多次尝试是否能获取到值,每次获取间隔休眠次数秒 + :param el_wait_time: 等待时间,查找元素最多等待多少秒,默认 2 秒 :return: """ try: self.driver.implicitly_wait(2) - el = self.driver.find_element_by_xpath(xpath) + start_time = time.time() + while True: + if time.time() - start_time > el_wait_time: + logger.warning(f'查找元素 {xpath} 耗时超过 {el_wait_time} 秒') + + return None + + try: + # 此处只为找到元素,如果下面不需要验证元素是否有值的话,则使用此处找到的元素 + # 否则下面验值逻辑会重新找到该元素以使用,此处的 el 会被覆盖 + el = self.driver.find_element_by_xpath(xpath) + + break + except Exception: + pass + + num = 0 + while True: + if not verify_val: + break + + # 需要每次循环找到此元素,以确定元素的值是否发生变化 + el = self.driver.find_element_by_xpath(xpath) + + if el.tag_name == 'input': + val = el.get_attribute('value') + if val and len(val) > 0: + break + elif el.text != '': + break + + # 多次尝试无果则放弃 + if num > max_num_of_attempts: + break + num += 1 + + time.sleep(num) return el if EC.visibility_of(el) else None except Exception: @@ -408,24 +444,49 @@ def element_visibility_of(self, xpath: str) -> WebElement or None: finally: self.driver.implicitly_wait(Netflix.TIMEOUT) - def has_unknown_error_alert(self) -> bool: + def has_unknown_error_alert(self, error_el_xpath: str) -> bool: """ - Netflix 提醒页面出现未知错误 + 页面提示未知错误 :return: """ - error_tips_el = self.element_visibility_of('//div[@class="ui-message-contents"]') - + error_tips_el = self.element_visibility_of(error_el_xpath, True) if error_tips_el: # 密码修改成功画面的提示语与错误提示语共用的同一个元素,防止误报 if 'YourAccount?confirm=password' in self.driver.current_url or 'Your password has been changed' in error_tips_el.text: return False - logger.warning('页面出现未知错误提醒,提醒内容为 ' + error_tips_el.text) + logger.warning(f'页面出现未知错误:{error_tips_el.text}') return True return False + def handle_click_events(self, func, error_el_xpath='//div[@class="ui-message-contents"]', + max_num_of_attempts: int = 10): + """ + 处理点击事件 + + 在某些画面点击提交的时候,有可能报未知错误,需要稍等片刻再点击才正常 + :param func: + :param max_num_of_attempts: + :return: + """ + func() + + num = 0 + while True: + if self.has_unknown_error_alert(error_el_xpath): + func() + + if num >= max_num_of_attempts: + raise Exception('处理未知错误失败') + num += 1 + + logger.info(f'程式将休眠 {num} 秒后重试点击动作') + time.sleep(num) + else: + break + def handle_unknown_error_alert(self, func, max_try: int = 10): """ 处理 Netflix 未知异常 @@ -467,9 +528,8 @@ def __reset_password_via_mail(self, reset_url: str, new_netflix_password: str) - self.driver.get(reset_url) self.input_pwd(new_netflix_password) - self.click_submit_btn() - self.handle_unknown_error_alert(self.click_submit_btn) + self.handle_click_events(self.click_submit_btn) # 如果奈飞提示密码曾经用过,则应该先改为随机密码,然后再改回来 pwd_error_tips = self.element_visibility_of('//div[@data-uia="field-newPassword+error"]') @@ -479,9 +539,8 @@ def __reset_password_via_mail(self, reset_url: str, new_netflix_password: str) - random_pwd = self.gen_random_pwd() self.input_pwd(random_pwd) - self.click_submit_btn() - self.handle_unknown_error_alert(self.click_submit_btn) + self.handle_click_events(self.click_submit_btn) if not self.__pwd_change_result(): return False diff --git a/resources/Netflix_Logo_RGB.png b/resources/Netflix_Logo_RGB.png new file mode 100644 index 0000000000000000000000000000000000000000..151775b3b17bc46f78a55856659ad924d6f3a9de GIT binary patch literal 16470 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 fCY0qJYI;`nP$+dZ9n~Ube8~R;VRQyx literal 0 HcmV?d00001