From 0bc367816de92fa3499f7aaca663c066af7acf6d Mon Sep 17 00:00:00 2001 From: LiGu Date: Wed, 4 Sep 2024 12:09:23 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E5=AE=8C=E5=96=84tree=5Fview=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fucker.py | 29 +++++++++++++++-------------- main.py | 8 ++++++-- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/fucker.py b/fucker.py index b416223..fd43788 100644 --- a/fucker.py +++ b/fucker.py @@ -55,7 +55,8 @@ def __init__(self, cookies: dict = None, speed: float = None, end_thre: float = None, pushplus_token: str = '', - bark_token: str = ''): + bark_token: str = '', + tree_view:bool = True): """ ### Fucker Class * `cookies`: dict, optional, cookies to use for the session @@ -64,6 +65,7 @@ def __init__(self, cookies: dict = None, * `limit`: int, optional, time limit for each course, in minutes (default is 0), auto resets on fuck*Course methods call * `speed`: float, optional, video playback speed * `end_thre`: float, optional, threshold to stop the fucker, overloaded when there are questions left unanswered + * `tree_view` :bool, optional, print the tree progress view of the course """ logger.debug(f"created a Fucker {id(self)}, limit: {limit}, speed: {speed}, end_thre: {end_thre}") @@ -98,6 +100,7 @@ def __init__(self, cookies: dict = None, self.courses = ObjDict(default=None) # store courses info self._pushplus = partial(pushpluser, token=pushplus_token) if pushplus_token else lambda *args, **kwargs: None self._bark = partial(barkpusher, token=bark_token) if bark_token else lambda *args, **kwargs: None + self.tree_view = tree_view @property # cannot directly manipulate _cookies property, we need to parse uuid from cookies def cookies(self) -> RequestsCookieJar: @@ -290,16 +293,15 @@ def fuckWhatever(self): logger.exception(e) continue - def fuckCourse(self, course_id:str, tree_view:bool=True): + def fuckCourse(self, course_id:str): """ ### Fuck the whole course * `course_id`: `courseId`(Hike) or `recuitAndCourseId`(Zhidao) - * `tree_view`: whether to print the tree view of the progress """ if re.match(r".*[a-zA-Z].*", course_id): # determine if it's a courseId or a recruitAndCourseId - self.fuckZhidaoCourse(course_id, tree_view=tree_view) # it's a recruitAndCourseId + self.fuckZhidaoCourse(course_id) # it's a recruitAndCourseId else: # it's a courseId - self.fuckHikeCourse(course_id, tree_view=tree_view) + self.fuckHikeCourse(course_id) def fuckVideo(self, course_id, video_id:str): """ @@ -406,13 +408,12 @@ def getZhidaoContext(self, RAC_id:str, force:bool=False): self.context[RAC_id] = ctx return ctx - def fuckZhidaoCourse(self, RAC_id:str, tree_view:bool=True): + def fuckZhidaoCourse(self, RAC_id:str): """ * `RAC_id`: `recruitAndCourseId` - * `tree_view`: whether to print the tree progress view of the course """ logger.info(f"Fucking Zhidao course {RAC_id}") - tprint = print if tree_view else lambda *a, **k: None + tprint = print if self.tree_view else lambda *a, **k: None # load context ctx = self.getZhidaoContext(RAC_id) @@ -855,8 +856,8 @@ def getHikeContext(self, course_id:str, force:bool=False): self.context[course_id] = ctx return ctx - def fuckHikeCourse(self, course_id:str, tree_view:bool=True): - tprint = print if tree_view else lambda *a, **k: None + def fuckHikeCourse(self, course_id:str): + tprint = print if self.tree_view else lambda *a, **k: None begin_time = time.time() root = self.getHikeContext(course_id).root @@ -865,7 +866,7 @@ def fuckHikeCourse(self, course_id:str, tree_view:bool=True): tprint(f"Fucking course {course_id} (total root chapters: {len(root)})") try: for chapter in root: - self._traverse(course_id, chapter, tree_view=tree_view) + self._traverse(course_id, chapter) except KeyboardInterrupt: logger.info("user interrupted") logger.info(f"Fucked course {course_id}, cost {time.time()-begin_time}s") @@ -912,9 +913,9 @@ def fuckFile(self, course_id, file_id): self.stuViewFile(course_id, file_id) time.sleep(random()*2+1) # more human-like - def _traverse(self,course_id, node: ObjDict, depth=0, tree_view=True): + def _traverse(self,course_id, node: ObjDict, depth=0): depth += 1 - tprint = print if tree_view else lambda *a, **k: None + tprint = print if self.tree_view else lambda *a, **k: None w_lim = os.get_terminal_size().columns-1 # width limit for terminal output prefix = self.prefix * depth if node.childList: # if childList is not None, then it's a chapter @@ -923,7 +924,7 @@ def _traverse(self,course_id, node: ObjDict, depth=0, tree_view=True): tprint(prefix) # separate chapters tprint(f"{prefix}__Fucking chapter {chapter.name}"[:w_lim]) for child in chapter.childList: - self._traverse(course_id, child, depth=depth, tree_view=tree_view) + self._traverse(course_id, child, depth=depth) else: # if childList is None, then it's a file file = node file.studyTime = file.studyTime or 0 # sometimes it's None diff --git a/main.py b/main.py index df13a58..357eef0 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,7 @@ "save_cookies": True, "proxies": {}, "logLevel": "INFO", + "tree_view": True, "qr_extra": { "show_in_terminal": None, "ensure_unicode": False @@ -83,6 +84,8 @@ action="store_true", help="Show QR in terminal") parser.add_argument("--proxy", type=str, help="Proxy Config, e.g: http://127.0.0.1:8080") +parser.add_argument("--tree_view", type=bool, + help="print the tree progress view of the course") args = parser.parse_args() @@ -93,6 +96,7 @@ save_cookies = config.save_cookies or False qr_extra = config.qr_extra or ObjDict(default=None) show_in_terminal = args.show_in_terminal or config.qr_extra.show_in_terminal +tree_view = args.tree_view or config.tree_view if show_in_terminal is None: # Defaults to terminal in Windows show_in_terminal = platform.system() == "Windows" @@ -140,8 +144,8 @@ print("*Failed to check update\n") # create an instance, now we are talking... or fucking -fucker = Fucker(proxies=proxies, speed=args.speed, - end_thre=args.threshold, limit=args.limit, pushplus_token=pushplus_token, bark_token=bark_token) +fucker = Fucker(proxies=proxies, speed=args.speed, end_thre=args.threshold, limit=args.limit, + pushplus_token=pushplus_token, bark_token=bark_token, tree_view=tree_view) cookies_path = getRealPath("./cookies.json") cookies_loaded = False From eaf5f9b42a8a40c714b34b347b0d3da72a8107d9 Mon Sep 17 00:00:00 2001 From: LiGu Date: Wed, 4 Sep 2024 12:26:24 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=9C=A8=20nohup=20=E4=B8=8B=E8=BF=90=E8=A1=8C=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fucker.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fucker.py b/fucker.py index fd43788..6726406 100644 --- a/fucker.py +++ b/fucker.py @@ -424,7 +424,12 @@ def fuckZhidaoCourse(self, RAC_id:str): tprint(f"Fucking Zhidao course: {course.courseInfo.name or course.courseInfo.enName}") begin_time = time.time() # real world time prefix = self.prefix # prefix for tree-like print - w_lim = os.get_terminal_size().columns-1 # width limit for terminal output + try: + # 在 nohup 下运行无法获取,进行捕获 + w_lim = os.get_terminal_size().columns-1 # width limit for terminal output + except Exception as e: + # 考虑直接移除此变量,但是保留原代码风格,故进行赋值 + w_lim = 80 try: for chapter in chapters.videoChapterDtos: tprint(prefix) # extra line as separator @@ -916,7 +921,12 @@ def fuckFile(self, course_id, file_id): def _traverse(self,course_id, node: ObjDict, depth=0): depth += 1 tprint = print if self.tree_view else lambda *a, **k: None - w_lim = os.get_terminal_size().columns-1 # width limit for terminal output + try: + # 在 nohup 下运行无法获取,进行捕获 + w_lim = os.get_terminal_size().columns-1 # width limit for terminal output + except Exception as e: + # 考虑直接移除此变量,但是保留原代码风格,故进行赋值 + w_lim = 80 prefix = self.prefix * depth if node.childList: # if childList is not None, then it's a chapter chapter = node From f58bbc88b052636291d2de1bedcf3184d413f6a3 Mon Sep 17 00:00:00 2001 From: LiGu <89311746+gulideshanhe@users.noreply.github.com> Date: Wed, 4 Sep 2024 13:58:33 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fucker.py | 9 ++++++++- main.py | 9 +++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/fucker.py b/fucker.py index 6726406..f5978b0 100644 --- a/fucker.py +++ b/fucker.py @@ -56,7 +56,8 @@ def __init__(self, cookies: dict = None, end_thre: float = None, pushplus_token: str = '', bark_token: str = '', - tree_view:bool = True): + tree_view:bool = True, + image_path:str = ""): """ ### Fucker Class * `cookies`: dict, optional, cookies to use for the session @@ -101,6 +102,7 @@ def __init__(self, cookies: dict = None, self._pushplus = partial(pushpluser, token=pushplus_token) if pushplus_token else lambda *args, **kwargs: None self._bark = partial(barkpusher, token=bark_token) if bark_token else lambda *args, **kwargs: None self.tree_view = tree_view + self.image_path = image_path @property # cannot directly manipulate _cookies property, we need to parse uuid from cookies def cookies(self) -> RequestsCookieJar: @@ -193,6 +195,11 @@ def _qrlogin(self, qr_callback): r = self.session.get(qr_page, timeout=10).json() qrToken = r["qrToken"] img = b64decode(r["img"]) + if self.image_path != "": # 路径非空时保存图片到指定路径 + image_path = f"{os.path.join(self.image_path, time.strftime('%Y-%m-%dT%H-%M-%S'))}.png" + with open(image_path, "wb") as f: + f.write(img) + logger.info(f"图片已保存至{image_path}") qr_callback(img) logger.debug(f"QR login received, token{qrToken}") scanned = False diff --git a/main.py b/main.py index 357eef0..1b910df 100644 --- a/main.py +++ b/main.py @@ -24,6 +24,7 @@ "show_in_terminal": None, "ensure_unicode": False }, + "image_path":"", "pushplus": { "enable": False, "token": "" @@ -36,7 +37,8 @@ } # get config or create one if not exist if os.path.isfile(getConfigPath()): - with open(getConfigPath(), 'r+') as f: + with open(getConfigPath(), 'r+', encoding="UTF-8") as f: + # 不指定编码格式会导致config中可能存在的中文字符乱码 config = ObjDict(json.load(f), default=None) if "config_version" not in config: config.config_version = "1.0.0" @@ -86,6 +88,8 @@ help="Proxy Config, e.g: http://127.0.0.1:8080") parser.add_argument("--tree_view", type=bool, help="print the tree progress view of the course") +parser.add_argument("--image_path", type=str, + help="Image save path, default is empty (do not save)") args = parser.parse_args() @@ -97,6 +101,7 @@ qr_extra = config.qr_extra or ObjDict(default=None) show_in_terminal = args.show_in_terminal or config.qr_extra.show_in_terminal tree_view = args.tree_view or config.tree_view +image_path = args.image_path or config.image_path if show_in_terminal is None: # Defaults to terminal in Windows show_in_terminal = platform.system() == "Windows" @@ -145,7 +150,7 @@ # create an instance, now we are talking... or fucking fucker = Fucker(proxies=proxies, speed=args.speed, end_thre=args.threshold, limit=args.limit, - pushplus_token=pushplus_token, bark_token=bark_token, tree_view=tree_view) + pushplus_token=pushplus_token, bark_token=bark_token, tree_view=tree_view, image_path=image_path) cookies_path = getRealPath("./cookies.json") cookies_loaded = False From 105f46f1609ed3c45a73dd2209b8e50f0ce9d8b4 Mon Sep 17 00:00:00 2001 From: LiGu <89311746+gulideshanhe@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:31:35 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E8=BE=93=E5=87=BA=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E6=9D=A1=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fucker.py | 7 ++++--- main.py | 6 +++++- utils.py | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/fucker.py b/fucker.py index f5978b0..e329b26 100644 --- a/fucker.py +++ b/fucker.py @@ -57,6 +57,7 @@ def __init__(self, cookies: dict = None, pushplus_token: str = '', bark_token: str = '', tree_view:bool = True, + progressbar_view:bool = True, image_path:str = ""): """ ### Fucker Class @@ -102,6 +103,7 @@ def __init__(self, cookies: dict = None, self._pushplus = partial(pushpluser, token=pushplus_token) if pushplus_token else lambda *args, **kwargs: None self._bark = partial(barkpusher, token=bark_token) if bark_token else lambda *args, **kwargs: None self.tree_view = tree_view + self.progressbar_view = progressbar_view self.image_path = image_path @property # cannot directly manipulate _cookies property, we need to parse uuid from cookies @@ -581,7 +583,7 @@ def fuckZhidaoVideo(self, RAC_id, video_id): # have a glance of when quiz is answered action = "pause a minute" if pause else \ f"fucking {video.videoId}" if answer is None else "answering quiz" - progressBar(s, e, prefix=action, suffix="done") + progressBar(s, e, prefix=action, suffix="done", progressbar_view=self.progressbar_view) ##### end main event loop time.sleep(random()+1) # old Joe needs more sleep @@ -916,8 +918,7 @@ def fuckHikeVideo(self, course_id, file_id, prev_time=0): not (int(played_time-prev_time) % interval): ret_time = self.saveStuStudyRecord(course_id,file_id,played_time,prev_time,start_date) # report progress prev_time, played_time = ret_time, ret_time - progressBar(played_time, end_time, - prefix=f"fucking {file_id}", suffix="done") + progressBar(played_time, end_time, prefix=f"fucking {file_id}", suffix="done", progressbar_view=self.progressbar_view) logger.info(f"Fucked video {file_id} of course {course_id}, cost {time.time()-begin_time:.2f}s") time.sleep(random()+1) # more human-like diff --git a/main.py b/main.py index 1b910df..0969133 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,7 @@ "proxies": {}, "logLevel": "INFO", "tree_view": True, + "progressbar_vier": True, "qr_extra": { "show_in_terminal": None, "ensure_unicode": False @@ -88,6 +89,8 @@ help="Proxy Config, e.g: http://127.0.0.1:8080") parser.add_argument("--tree_view", type=bool, help="print the tree progress view of the course") +parser.add_argument("--progressbar_view", type=bool, + help="print the progressbar view of the course") parser.add_argument("--image_path", type=str, help="Image save path, default is empty (do not save)") @@ -101,6 +104,7 @@ qr_extra = config.qr_extra or ObjDict(default=None) show_in_terminal = args.show_in_terminal or config.qr_extra.show_in_terminal tree_view = args.tree_view or config.tree_view +progressbar_view = args.progressbar_view or config.progressbar_view image_path = args.image_path or config.image_path if show_in_terminal is None: # Defaults to terminal in Windows @@ -150,7 +154,7 @@ # create an instance, now we are talking... or fucking fucker = Fucker(proxies=proxies, speed=args.speed, end_thre=args.threshold, limit=args.limit, - pushplus_token=pushplus_token, bark_token=bark_token, tree_view=tree_view, image_path=image_path) + pushplus_token=pushplus_token, bark_token=bark_token, tree_view=tree_view,progressbar_view=progressbar_view, image_path=image_path) cookies_path = getRealPath("./cookies.json") cookies_loaded = False diff --git a/utils.py b/utils.py index ab0ade4..14a7131 100644 --- a/utils.py +++ b/utils.py @@ -94,7 +94,7 @@ def wipeLine(): print('\r' + ' ' * (width), end = '\r', flush=True) def progressBar (iteration, total, prefix = '', suffix = '', decimals = 1, - length = None, fill = '#'): + length = None, fill = '#', progressbar_view:bool = True): """ ### Call in a loop to create terminal progress bar * `iteration` - Required : current iteration (Int) @@ -105,6 +105,8 @@ def progressBar (iteration, total, prefix = '', suffix = '', decimals = 1, * `length` - Optional : character length of bar (Int) * `fill` - Optional : bar fill character (Str) """ + if not progressbar_view: + return False if not length: try: length = os.get_terminal_size().columns - 4 From 362e5ff22da898ee5c3f0fe0b7d31a11d95753ea Mon Sep 17 00:00:00 2001 From: LiGu <89311746+gulideshanhe@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:32:55 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8C=87=E5=8C=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bf4fc68..c3df6cd 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ ### Login -_\*如果非常用地登入会需要短信验证, 您应该先用浏览器登入一次, 以让您的所在地列入白名单._ +_\*如果非常用地登入(可能?)会需要短信验证, 您应该先用浏览器登入一次, 以让您的所在地列入白名单。_ _\*\*信息优先级: 命令行 > 配置文件 > 交互输入_ ### 使用配置文件 @@ -90,24 +90,37 @@ _\*如果非常用地登入会需要短信验证, 您应该先用浏览器登入 "username": "", "password": "", "qrlogin": true, + "save_cookies": true, "proxies": {}, "logLevel": "INFO", + "tree_view": true, + "progressbar_vier": false, "qr_extra": { - "show_in_terminal": false, - "ensure_unicode": false - }, - "push": { - "enable": false, - "token": "", - } + "show_in_terminal": null, + "ensure_unicode": false + }, + "image_path": "", + "pushplus": { + "enable": false, + "token": "" + }, + "bark": { + "enable": false, + "token": "https://example.com/xxxxxxxxx" + }, + "config_version": "1.3.0" } ``` - `username`: 账号 - `password`: 密码 -- `qrlogin`: 启用二维码登陆, 方便在服务器上部署, 优先级高于账号密码 +- `qrlogin`: **目前强制启用**二维码登陆, 方便在服务器上部署, 优先级高于账号密码 - `proxies`: 代理, 可留空, 在 _Windows_ 上还可解决 _Clash_ 等代理造成的证书错误, 详见 [_常见问题_](https://github.com/VermiIIi0n/fuckZHS/discussions/25) - `logLevel`: 日志等级, 可选 `NOTSET` `DEBUG` `INFO` `WARNING` `ERROR` `CRITICAL` +- `save_cookies`: 保存cookies,*短时间*内可以自动登录。 +- `tree_view`: 课程目录结构,关闭后不显示所有课程目录。 +- `progressbar_vier`: 进度条控制,关闭后不显示当前视频进度。 +- `image_path`: 登录二维码保存路径,留空则不保存。 - `qr_extra`: QR 相关配置 - `show_in_terminal`: 将二维码打印至终端 - `ensure_unicode`: 仅使用 Unicode 字符打印二维码 @@ -118,7 +131,7 @@ _\*如果非常用地登入会需要短信验证, 您应该先用浏览器登入 - `enable`: 启用推送 - `token`: 推送 token,例如:`https://api.day.app/xxxxxxxxxxxxx` -~填入账号密码即可无干预自动登入~ 当前失效 +~~填入账号密码即可无干预自动登入~~ **当前失效** _\*配置文件如果没有的话会在 main.py 执行时自动创建._ ### 使用命令行参数登入 @@ -192,7 +205,7 @@ python main.py --fetch - `-v`, `--videos`: 视频 ID, `fileId` 或 `videoId`, 可输入多个 - `-u`, `--username`: 账号 - `-p`, `--password`: 密码 -- `-q`, `--qrlogin`: +- `-q`, `--qrlogin`: 二维码登录,目前**强制开启** - `-s`, `--speed`: ~~**POWERR AND SPEEEEED!**~~ 指定播放速度, 想要秒过可以设个很高的值(e.g. 644), 但不推荐. 默认为浏览器观看能到的最大值 - `-t`, `--threshold`: 完成时播放百分比, 高于该值视作完成, 想要重复刷课可以使用大于 `1.0` 的值, 例如 `2.0` 则会再刷一遍 - `-l`, `--limit`: 单节课的时限, 如果您看得上内点习惯分就用吧 @@ -200,6 +213,9 @@ python main.py --fetch - `-f`, `--fetch`: 获取课程清单并存入 _execution.json_ 文件 - `--show_in_terminal`: 将二维码打印至终端 - `--proxy`: 代理设置, 本来用来调试的(e.g. ) +- `--tree_view`: 课程目录结构,关闭后不显示所有课程目录。 +- `--progressbar_view`: 进度条控制,关闭后不显示当前视频进度。 +- `--image_path`: 登录二维码保存路径,留空则不保存。 - `-h` `--help`: 显示帮助 运行示例如下: