Skip to content

Commit

Permalink
fix for #10
Browse files Browse the repository at this point in the history
  • Loading branch information
VermiIIi0n committed Sep 7, 2022
1 parent 1485bc5 commit 923b2f8
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 36 deletions.
55 changes: 34 additions & 21 deletions ObjDict.py
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
1. 新增二维码登陆, 当前版本强制启用(由于登陆验证改变, 目前账号密码登陆失效) 详见[Login](#Login)
2. 新增依赖 _websockets_
3. 新增依赖 _Pillow_
4. 没时间维护, 代码屎山化严重, API文档部分过时, 请见谅

-> v2.2.0:
1. 课程 ID 不再为必须参数
Expand Down
69 changes: 63 additions & 6 deletions fucker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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'''
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 10 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"+
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2.3.3",
"version": "2.3.4",
"branch": "master",
"author": "VermiIIi0n"
}
1 change: 1 addition & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 4 additions & 4 deletions zd_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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))
print(revEv(r.sdsew))

0 comments on commit 923b2f8

Please sign in to comment.