diff --git a/README.md b/README.md index 828eb07..17968f0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,31 @@ <div align="center"> + +[![Netflix.png](https://s4.ax1x.com/2021/12/24/TYGHXD.png)](https://s4.ax1x.com/2021/12/24/TYGHXD.png) + <h1>Netflix</h1> 监听奈飞(Netflix)密码变更邮件,自动重置密码。 </div> -### 简介 +### 缘起 + +共享 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 0000000..151775b Binary files /dev/null and b/resources/Netflix_Logo_RGB.png differ