From 923b2f816fa7d5186a37cdbbb0ad4edb5290bddf Mon Sep 17 00:00:00 2001 From: Vermilli0n <104612478+VermiIIi0n@users.noreply.github.com> Date: Wed, 7 Sep 2022 17:54:15 +0800 Subject: [PATCH] fix for #10 --- ObjDict.py | 55 ++++++++++++++++++++++++++---------------- README.md | 1 + fucker.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++----- main.py | 14 +++++++---- meta.json | 2 +- utils.py | 1 + zd_utils.py | 8 +++---- 7 files changed, 114 insertions(+), 36 deletions(-) diff --git a/ObjDict.py b/ObjDict.py index 694f984..ae3e40c 100644 --- a/ObjDict.py +++ b/ObjDict.py @@ -1,34 +1,42 @@ # -*- coding: utf-8 -*- +from __future__ import annotations +from typing import Dict, Any, Optional from copy import deepcopy + class ObjDict(dict): @property - def NotExist(self): # for default value + def NotExist(self): # for default value return ObjDict.NotExist - - def __init__(self, d:dict=None, recursive=True, default=NotExist, *, antiloop_map = None): + + def __init__(self, d: dict = None, recursive=True, default=NotExist, *, antiloop_map=None): ''' ## ObjDict is a subclass of dict that allows for object-like access #### Preserved: - these preserved names are not allowed to be set using dot access, but you can access your version using `['name']` or `get` + these preserved names are not allowed to be set using dot access, + but you can access your version using `['name']` or `get` * `NotExist`: default value for missing key, will raise KeyError * `update`: just like dict.update(), but recursively converts nested dicts * `copy`: returns a shallow copy * Any attribute of the dict class * Any name starts with `_` - #### Precedence: + #### Precedence: * `.` : Attribute > Key > Default * `[]` & `get` : Key > Default #### Params: * `d`: dict - * `default`: default value to return if key is not found, reset to ObjDict.NotExist to raise KeyError + * `default`: default value to return if key is not found, + reset to ObjDict.NotExist to raise KeyError * `recursive`: recursively try to convert all sub-objects in `d` - * `antiloop_map`: a dict to store the loop-detection, if you want to use the same ObjDict object in multiple places, you can pass a dict to `antiloop_map` to avoid infinite loop + * `antiloop_map`: a dict to store the loop-detection, + if you want to use the same ObjDict object in multiple places, + you can pass a dict to `antiloop_map` to avoid infinite loop ''' - self.__dict__["_antiloop_map"] = {} if antiloop_map is None else antiloop_map # for reference loop safety + self.__dict__["_antiloop_map"] = { + } if antiloop_map is None else antiloop_map # for reference loop safety self.__dict__["_default"] = default self.__dict__["_recursive"] = recursive d = d or {} @@ -40,13 +48,14 @@ def update(self, d, **kw): if not isinstance(d, dict) or kw: d = dict(d, **kw) else: - self._convert(d) # create a dummy if not exist yet, prevent infinite-loop + # create a dummy if not exist yet, prevent infinite-loop + self._convert(d) for k, v in d.items(): self[k] = self._convert(v) finally: - self.__dict__["_antiloop_map"] = {} # reset the map + self.__dict__["_antiloop_map"] = {} # reset the map - def _convert(self, v, recursive:bool=None): + def _convert(self, v: Any, recursive: Optional[bool] = None) -> Any: recursive = recursive if recursive is not None else self._recursive if not recursive: return v @@ -84,27 +93,29 @@ def default(self, value): self.__dict__["_default"] = value self.update(self) - def copy(self): + def copy(self) -> ObjDict: """### returns a shallow copy""" return ObjDict(self, recursive=False, default=self.default) - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: try: return self[name] except KeyError: raise AttributeError(f"{name} not found in {self}") - def __setattr__(self, name, value): + def __setattr__(self, name: str, value): if name in {"NotExist", "update", "copy"} or name.startswith("_"): - raise AttributeError(f"set '{name}' with dot access is not allowed, consider using ['{name}']") - if name in self.__dict__: # cannot just call setattr(self, name, value), recursion error + raise AttributeError( + f"set '{name}' with dot access is not allowed, consider using ['{name}']") + # cannot just call setattr(self, name, value), recursion error + if name in self.__dict__: self.__dict__[name] = value elif hasattr(getattr(type(self), name, None), "__set__"): getattr(type(self), name).__set__(self, value) else: self[name] = value - def __getitem__(self, name): + def __getitem__(self, name: str): if name in self: return self.get(name) elif self.default is ObjDict.NotExist: @@ -113,7 +124,9 @@ def __getitem__(self, name): self[name] = deepcopy(self.default) return self[name] - def __deepcopy__(self, memo): - shadow = dict(self) - copy = deepcopy(shadow, memo) - return ObjDict(copy, recursive=self.__dict__["_recursive"], default=self.default) + def __deepcopy__(self, memo: Dict[int, Any]): + copy = ObjDict({}, recursive=self.__dict__["_recursive"], default=self.default) + memo[id(self)] = copy + dummy = deepcopy(dict(self), memo) + copy.update(dummy) + return copy diff --git a/README.md b/README.md index f6c61e8..e8e2a39 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ 1. 新增二维码登陆, 当前版本强制启用(由于登陆验证改变, 目前账号密码登陆失效) 详见[Login](#Login) 2. 新增依赖 _websockets_ 3. 新增依赖 _Pillow_ +4. 没时间维护, 代码屎山化严重, API文档部分过时, 请见谅 -> v2.2.0: 1. 课程 ID 不再为必须参数 diff --git a/fucker.py b/fucker.py index 6e756b8..5dc68fc 100644 --- a/fucker.py +++ b/fucker.py @@ -10,10 +10,10 @@ from ObjDict import ObjDict from logger import logger from sign import sign +import urllib.request import websockets import requests import asyncio -import urllib import math import time import json @@ -317,7 +317,7 @@ def getZhidaoContext(self, RAC_id:str, force:bool=False): logger.info(f"{len(lesson_ids)} lessons, {len(videos)} videos") # get read-before, maybe unneccessary. BUTT hey, it's a POST request - self.queryStudyReadBefore(course_id, recruit_id) + # self.queryStudyReadBefore(course_id, recruit_id) # get study info, including watchState, studyTotalTime video_ids = [video.id for video in videos.values() if video.id] @@ -420,6 +420,9 @@ def fuckZhidaoVideo(self, RAC_id, video_id): # emulating video playing self.watchVideo(video.videoId) + # no idea what it is + self.threeDimensionalCourseWare(video.videoId) + # prepare vars speed = self.speed or 1.5 # default speed for Zhidao is 1.5 last_submit = played_time # last pause time @@ -473,11 +476,11 @@ def fuckZhidaoVideo(self, RAC_id, video_id): report = False # unset report flag wp.add(played_time) # now submit to database - self.saveDatabaseIntervalTime(RAC_id,video_id,played_time,last_submit,wp.get(),token_id) + self.saveDatabaseIntervalTimeV2(RAC_id,video_id,played_time,last_submit,wp.get(),token_id) last_submit = played_time # update last pause time wp.reset(played_time) # reset watch point ## report to cache - if elapsed_time % cache_interval == 0: + if False and elapsed_time % cache_interval == 0: wp.add(played_time) self.saveCacheIntervalTime(RAC_id,video_id,played_time,last_submit,wp.get(),token_id) last_submit = played_time # update last pause time @@ -503,9 +506,12 @@ def zhidaoQuery(self, url:str, data:dict, encrypt:bool=True, ok_code:int=0, """set ok_code to None for no check""" cipher = Cipher(key) if setTimeStamp: - data["dateFormate"] = int(time.time())*1000 # somehow their timestamps are ending with 000 + _t = int(time.time())*1000 # somehow their timestamps are ending with 000 + data["dateFormate"] = _t logger.debug(f"{method} url: {url}\nraw_data: {json.dumps(data,indent=4,ensure_ascii=False)}") form ={"secretStr": cipher.encrypt(json.dumps(data))} if encrypt else data + if setTimeStamp: + form["dateFormate"] = _t ret = self._apiQuery(url, data=form, method=method) if ok_code is not None and ret.code != ok_code: ret.default = None @@ -537,7 +543,7 @@ def videoList(self, RAC_id): def queryStudyReadBefore(self, course_id, recruit_id): '''### query study read before for zhidao share course''' read_url = "https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/queryStudyReadBefore" - return self.zhidaoQuery(read_url, data={"courseId": course_id, "recruitId": recruit_id}).data + return self.zhidaoQuery(read_url, data={"courseId": course_id, "recruitId": recruit_id}, ok_code=None).data def queryStudyInfo(self, lesson_ids:list, video_ids:list, recruit_id): '''### query study info for zhidao''' @@ -647,6 +653,57 @@ def saveDatabaseIntervalTime(self, RAC_id, video_id, played_time, last_submit, w } return self.zhidaoQuery(record_url, data=data).data + def threeDimensionalCourseWare(self, video_id): + '''### query three dimensional course ware for zhidao''' + ware_url = "https://studyservice-api.zhihuishu.com/gateway/t/v1/course/threeDimensionalCourseWare" + params = {"videoId": video_id} + return self.zhidaoQuery(ware_url, data=params, method="GET").data + + def saveDatabaseIntervalTimeV2(self, RAC_id, video_id, played_time, last_submit, watch_point, token_id=None, initial=False): + '''### save database interval time for zhidao''' + record_url = "https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/saveDatabaseIntervalTimeV2" + ctx = self.getZhidaoContext(RAC_id) + recruit_id = ctx.course.recruitId + video = ctx.videos[video_id] + if initial: # sometimes a request like this happens, I originally thought it is the initialization request, but I might be wrong + raw_ev = [ + recruit_id, + video.chapterId, # this.chapterId + ctx.course.courseInfo.courseId, + video.lessonId, # this.smallLessonId + HMS(seconds=min(video.videoSec, int(played_time))) , + int(played_time), + video.videoId, # this.videoId + '0', # this.data.studyStatus, always 0 + int(played_time), # this.totalStudyTime + self.uuid + ] + else: + raw_ev = [ + recruit_id, + video.lessonId, # this.lessonId + video.id, # this.smallLessonId + video.videoId, # this.videoId + video.chapterId, # this.chapterId + '0', # this.data.studyStatus, always 0 + int(played_time-last_submit), # this.playTimes + int(played_time), # this.totalStudyTime + HMS(seconds=min(video.videoSec, int(played_time))), + self.uuid + "zhs" + ] + if not token_id: + token_id = self.prelearningNote(RAC_id, video_id).studiedLessonDto.id + token_id = b64encode(str(token_id).encode()).decode() + data = { + "ewssw": watch_point, + "sdsew": getEv(raw_ev), + "zwsds": token_id, + "courseId": ctx.course.courseInfo.courseId + } + if initial: + data.pop("courseId") + return self.zhidaoQuery(record_url, data=data).data + def saveCacheIntervalTime(self, RAC_id, video_id, played_time, last_submit, watch_point, token_id=None): '''### save cache interval time for zhidao''' cache_url = "https://studyservice-api.zhihuishu.com/gateway/t/v1/learning/saveCacheIntervalTime" diff --git a/main.py b/main.py index dcfec3d..64d05ec 100644 --- a/main.py +++ b/main.py @@ -20,12 +20,19 @@ "show_in_terminal": False, "char_width": 2, "ensure_unicode": False - } + }, + "config_version": "1.0.0" } # get config or create one if not exist if os.path.isfile(getConfigPath()): with open(getConfigPath(), 'r') as f: config = ObjDict(json.load(f), default=None) + if "config_version" not in config or versionCmp(config.config_version, DEFAULT_CONFIG["config_version"]) < 0: + new = ObjDict(DEFAULT_CONFIG) + new.update(config) + config = new + with open(getConfigPath(), 'w') as f: + json.dump(config, f, indent=4) else: config = ObjDict(DEFAULT_CONFIG, default=None) with open(getConfigPath(), 'w') as f: @@ -58,8 +65,8 @@ qr_char_width = args.qr_char_width or qr_extra.char_width or 1 qr_ensure_unicode = args.qr_ensure_unicode or qr_extra.ensure_unicode or False show_in_terminal = args.show_in_terminal or qr_extra.show_in_terminal or False -logger.setLevel("DEBUG" if args.debug else config.logLevel) -proxies = config.proxies +logger.setLevel("DEBUG" if args.debug else (config.logLevel or "WARNING")) +proxies = config.proxies or {} if logger.getLevel() == "DEBUG": print("*****************************\n"+ @@ -107,7 +114,6 @@ ### first you need to login to get cookies try: if qrlogin: - print("Scan QR code") callback = partial(showImage, show_in_terminal=show_in_terminal ,char_width=qr_char_width, ensure_unicode=qr_ensure_unicode) fucker.login(use_qr=True, qr_callback=callback) else: diff --git a/meta.json b/meta.json index 7ad6422..fcb9cdb 100644 --- a/meta.json +++ b/meta.json @@ -1,5 +1,5 @@ { - "version": "2.3.3", + "version": "2.3.4", "branch": "master", "author": "VermiIIi0n" } \ No newline at end of file diff --git a/utils.py b/utils.py index 971d4cb..7e78241 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,7 @@ def showImage(img, show_in_terminal=False, **kw): else: img = Image.open(io.BytesIO(img)) img.show() + print("Scan QR code") def terminalShowImage(img, char_width=2, ensure_unicode=False): img = Image.open(io.BytesIO(img)) diff --git a/zd_utils.py b/zd_utils.py index 0db0257..e1cfde3 100644 --- a/zd_utils.py +++ b/zd_utils.py @@ -35,9 +35,9 @@ class WatchPoint: def __init__(self, init:int=0): self.reset(init) - def add(self, end:int, start:int=0): + def add(self, end:int, start:int=None): wp_interval = 2 # watch point record interval in seconds - start = int(start) or self.last + start = self.last if start is None else start end = int(end) self.last = end for i in range(start, end+1)[::wp_interval]: @@ -101,8 +101,8 @@ def gen(): h = Cipher(HOME_KEY) q = Cipher(QA_KEY) #print(getEv([1,2,3,4,'你'])) - d = "eo8zZpVghvx/CXsF1xTf6DaSVfioO/XS9PVwJh4HB6FiVIAVXT75rpsVuxmbt2kuAmzV2VSB1x6nEYX4+/tTHpO93D1DUC1jS0q5Gv0PFfNXjQRwLPLuhCVgaOOrejtvNngcG8ku5afL3heDnzamOrrrh+so8b+AkaNzp2NjowZesVmzpSOpVRx4EZbRCdxOV1qR4tWf1zTRVeDcbdrTq7y+rYDzuTK4DUCdgjmyU3w=" + d = "" r = ObjDict(json.loads(v.decrypt(d))) print(r) #print(r.watchPoint) - #print(revEv(r.ev)) \ No newline at end of file + print(revEv(r.sdsew)) \ No newline at end of file