├── .gitignore ├── LICENSE ├── README.md ├── config.yaml ├── pic └── test.jpg ├── requirements.txt └── src ├── app.py └── video.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Generated files 14 | .idea/**/contentModel.xml 15 | 16 | # Sensitive or high-churn files 17 | .idea/**/dataSources/ 18 | .idea/**/dataSources.ids 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | .idea/**/dbnavigator.xml 24 | 25 | # Gradle 26 | .idea/**/gradle.xml 27 | .idea/**/libraries 28 | 29 | # Gradle and Maven with auto-import 30 | # When using Gradle or Maven with auto-import, you should exclude module files, 31 | # since they will be recreated, and may cause churn. Uncomment if using 32 | # auto-import. 33 | # .idea/artifacts 34 | # .idea/compiler.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | ### VirtualEnv template 75 | # Virtualenv 76 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 77 | .Python 78 | [Bb]in 79 | [Ii]nclude 80 | [Ll]ib 81 | [Ll]ib64 82 | [Ll]ocal 83 | [Ss]cripts 84 | pyvenv.cfg 85 | .venv 86 | pip-selfcheck.json 87 | 88 | ### Python template 89 | # Byte-compiled / optimized / DLL files 90 | __pycache__/ 91 | *.py[cod] 92 | *$py.class 93 | 94 | # C extensions 95 | *.so 96 | 97 | # Distribution / packaging 98 | .Python 99 | build/ 100 | develop-eggs/ 101 | dist/ 102 | downloads/ 103 | eggs/ 104 | .eggs/ 105 | lib/ 106 | lib64/ 107 | parts/ 108 | sdist/ 109 | var/ 110 | wheels/ 111 | pip-wheel-metadata/ 112 | share/python-wheels/ 113 | *.egg-info/ 114 | .installed.cfg 115 | *.egg 116 | MANIFEST 117 | 118 | # PyInstaller 119 | # Usually these files are written by a python script from a template 120 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 121 | *.manifest 122 | *.spec 123 | 124 | # Installer logs 125 | pip-log.txt 126 | pip-delete-this-directory.txt 127 | 128 | # Unit test / coverage reports 129 | htmlcov/ 130 | .tox/ 131 | .nox/ 132 | .coverage 133 | .coverage.* 134 | .cache 135 | nosetests.xml 136 | coverage.xml 137 | *.cover 138 | *.py,cover 139 | .hypothesis/ 140 | .pytest_cache/ 141 | 142 | # Translations 143 | *.mo 144 | *.pot 145 | 146 | # Django stuff: 147 | *.log 148 | local_settings.py 149 | db.sqlite3 150 | db.sqlite3-journal 151 | 152 | # Flask stuff: 153 | instance/ 154 | .webassets-cache 155 | 156 | # Scrapy stuff: 157 | .scrapy 158 | 159 | # Sphinx documentation 160 | docs/_build/ 161 | 162 | # PyBuilder 163 | target/ 164 | 165 | # Jupyter Notebook 166 | .ipynb_checkpoints 167 | 168 | # IPython 169 | profile_default/ 170 | ipython_config.py 171 | 172 | # pyenv 173 | .python-version 174 | 175 | # pipenv 176 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 177 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 178 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 179 | # install all needed dependencies. 180 | #Pipfile.lock 181 | 182 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 183 | __pypackages__/ 184 | 185 | # Celery stuff 186 | celerybeat-schedule 187 | celerybeat.pid 188 | 189 | # SageMath parsed files 190 | *.sage.py 191 | 192 | # Environments 193 | .env 194 | .venv 195 | env/ 196 | venv/ 197 | ENV/ 198 | env.bak/ 199 | venv.bak/ 200 | 201 | # Spyder project settings 202 | .spyderproject 203 | .spyproject 204 | 205 | # Rope project settings 206 | .ropeproject 207 | 208 | # mkdocs documentation 209 | /site 210 | 211 | # mypy 212 | .mypy_cache/ 213 | .dmypy.json 214 | dmypy.json 215 | 216 | # Pyre type checker 217 | .pyre/ 218 | 219 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 !输认不永DGL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 蓝墨云助手 2 | 3 | 用来应付某些放了一堆课件在上面的垃圾sb课程的用来刷经验的垃圾脚本 4 | 5 | ### 功能介绍 6 | 7 | - [x] 刷课件/资源 8 | - [x] 视频文件 9 | - [x] 其他文件 10 | - [x] 每日经验值限制 11 | - [ ] 自动化做题/活动 12 | - [ ] 题库获取 13 | - [ ] 关键词/solr索引 14 | - [ ] 自动化提交答案/选项 15 | - [ ] 每日自动化获取资源/活动列表 16 | - [ ] schedule 17 | - [ ] 部署测试 18 | - [ ] 检测token是否过期并自动重新登录 19 | - [ ] 异步优化 20 | - [ ] 脚本化部署流程 21 | - [ ] run.sh 22 | - [ ] docker 23 | 24 | ### 使用教程 25 | 26 | - 安装依赖 27 | 28 | > pip install -r requirements.txt 29 | 30 | - 配置文件 31 | 32 | > 配置留空处 已经写好注释 33 | 34 | - 运行 35 | 36 | > python ./src/app.py 37 | 38 | ### 效果图 39 | 40 | ![test](pic/test.jpg) -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # 课程id 3 | course_id: 4 | # 每日要刷的经验值数量 5 | exp_speed: 30 6 | 7 | url_config: 8 | base_url: https://www.mosoteach.cn/web/index.php?c=res&m=index&clazz_course_id= 9 | video_url: https://www.mosoteach.cn/web/index.php?c=res&m=save_watch_to 10 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36 11 | # 这里填入你自己的cookie 12 | Cookie: 13 | # 防止token过期写的登录页面 post method 14 | login_url: https://www.mosoteach.cn/web/index.php?c=passport&m=account_login 15 | -------------------------------------------------------------------------------- /pic/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2017IOTrepo/BlueShitSupport/2e0d429885efcc72afdeca90316bd8c69c1c7c28/pic/test.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.6.2 2 | APScheduler==3.6.3 3 | async-timeout==3.0.1 4 | attrs==19.3.0 5 | beautifulsoup4==4.8.2 6 | certifi==2019.11.28 7 | chardet==3.0.4 8 | cssselect==1.1.0 9 | idna==2.8 10 | lxml==4.5.0 11 | multidict==4.7.4 12 | pyquery==1.4.1 13 | pytz==2019.3 14 | PyYAML==5.3 15 | requests==2.22.0 16 | six==1.14.0 17 | soupsieve==1.9.5 18 | tzlocal==2.0.0 19 | urllib3==1.25.8 20 | yarl==1.4.2 21 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | /** 3 | * author: xmmmmmovo 4 | * generation time: 2020/02/26 5 | * filename: app.py 6 | * language & build version : python3.6 7 | */ 8 | """ 9 | import os 10 | import asyncio 11 | from pprint import pprint 12 | import requests 13 | import yaml 14 | from pyquery import PyQuery 15 | 16 | from video import Mp4info 17 | 18 | config = None 19 | titles = [] 20 | urls = [] 21 | res_id = [] 22 | loop = asyncio.get_event_loop() 23 | 24 | 25 | def get(url, params=None): 26 | resp = requests.get(url=url, params=params, headers={ 27 | 'Cookie': config['url_config']['Cookie'], 28 | 'User-Agent': config['url_config']['User-Agent'], 29 | }) 30 | return resp.content if resp.status_code == 200 else None 31 | 32 | 33 | def post(url, data=None): 34 | resp = requests.post( 35 | url=url, 36 | data=data, 37 | headers={ 38 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 39 | 'Cookie': config['url_config']['Cookie'], 40 | 'User-Agent': config['url_config']['User-Agent'], 41 | } 42 | ) 43 | return resp.content if resp.status_code == 200 else None 44 | 45 | 46 | def file_write(name: str, data: list): 47 | file = open(f'cache/{name}.txt', 'w', encoding='utf8') 48 | file.write('\n'.join(data)) 49 | file.close() 50 | 51 | 52 | def config_load(): 53 | global config 54 | config = yaml.load( 55 | open(r'config.yaml', encoding='utf8'), 56 | Loader=yaml.FullLoader 57 | ) 58 | 59 | 60 | def get_course_src(): 61 | resp_text = get( 62 | config['url_config']['base_url'] + config['app']['course_id'] 63 | ) 64 | parser = PyQuery(resp_text) 65 | 66 | def fun_iter(i, element): 67 | dataIsDrag = parser(element) \ 68 | .find('.res-info') \ 69 | .find('.create-box') \ 70 | .children()[-4] \ 71 | .attrib['data-is-drag'] 72 | 73 | if dataIsDrag == 'N': 74 | titles.append( 75 | parser(element) 76 | .find('.res-info') 77 | .find('.overflow-ellipsis') 78 | .find('.res-name') 79 | .text() 80 | .strip() 81 | ) 82 | urls.append( 83 | parser(element) 84 | .attr('data-href') 85 | .strip() 86 | ) 87 | res_id.append( 88 | parser(element) 89 | .attr('data-value') 90 | .strip() 91 | ) 92 | 93 | parser('.hide-div') \ 94 | .children() \ 95 | .each(fun_iter) 96 | 97 | # file_write('urls', urls) 98 | # file_write('titles', titles) 99 | # file_write('res_id', res_id) 100 | 101 | 102 | async def video_score(i): 103 | file = Mp4info(urls[i]) 104 | d = file.get_duration() 105 | pprint(d) 106 | data = { 107 | 'clazz_course_id': config['app']['course_id'], 108 | 'res_id': res_id[i], 109 | 'watch_to': int(d) + 1, 110 | 'duration': int(d) + 1, 111 | 'current_watch_to': 0 112 | } 113 | resp = post( 114 | url=config['url_config']['video_url'], 115 | data=data 116 | ) 117 | pprint(str(titles[i]) + str(resp)) 118 | 119 | 120 | async def other_score(i): 121 | resp = get(url=urls[i]) 122 | pprint(str(titles[i]) + str("normal_complete")) 123 | 124 | 125 | async def score_async(i): 126 | p = os.path.splitext(titles[i]) 127 | pprint(p[0]) 128 | if p[1] == '.mp4': 129 | await video_score(i) 130 | else: 131 | await other_score(i) 132 | 133 | 134 | def score_main(): 135 | rate = int(config['app']['exp_speed'] / 2) 136 | for i in range(rate): 137 | loop.run_until_complete(score_async(i)) 138 | 139 | 140 | if __name__ == '__main__': 141 | pprint('--------读取配置文件--------') 142 | config_load() 143 | pprint(config) 144 | pprint('--------读取成功--------') 145 | pprint('--------获取网课资源列表--------') 146 | get_course_src() 147 | pprint('--------获取成功--------') 148 | pprint('--------开始刷资源--------') 149 | score_main() 150 | -------------------------------------------------------------------------------- /src/video.py: -------------------------------------------------------------------------------- 1 | import requests, struct 2 | from pprint import pprint 3 | 4 | 5 | class Mp4info: 6 | def __init__(self, file): 7 | self.file = file 8 | self.seek = 0 9 | self.duration = 0 10 | self.s = requests.session() 11 | self.timeout = 200 12 | self.s.headers = { 13 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 14 | 'Cookie': '_uab_collina=158099356218517362584412; acw_tc=707c9f9d15809935624926727e3de40a58560ba6a44dd4d3cab4b8d21211cc; login_token=4bc891c38b87c3048edb8b59493b42f725176e3408875bc1e96a5fc0109b47da; teachweb=d4f4172703f4ec24a7f6c1a4c16efa458b4d0b74; SERVERID=da350d2ba570698afee980c284304ee2|1581183791|1581183764', 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36' 16 | } 17 | 18 | # 设置请求头 19 | def _set_headers(self, seek): 20 | self.s.headers['Range'] = 'bytes={}-{}'.format(seek, seek + 7) 21 | 22 | def _send_request(self): 23 | try: 24 | data = self.s.get(url=self.file, stream=True, 25 | timeout=self.timeout).raw.read() 26 | except requests.Timeout: 27 | raise self.file + '连接超时:超过200秒(默认)服务器没有响应任何数据!' 28 | return data 29 | 30 | def _find_moov_request(self): 31 | self._set_headers(self.seek) 32 | data = self._send_request() 33 | size = int(struct.unpack('>I', data[:4])[0]) 34 | flag = data[-4:].decode('ascii') 35 | return size, flag 36 | 37 | def _find_duration_request(self): 38 | # 4+4是moov的大小和标识,跳过20个字符,直接读到time_scale,duration 39 | self._set_headers(seek=self.seek + 4 + 4 + 20) 40 | data = self._send_request() 41 | time_scale = int(struct.unpack('>I', data[:4])[0]) 42 | duration = int(struct.unpack('>I', data[-4:])[0]) 43 | return time_scale, duration 44 | 45 | def get_duration(self): 46 | while True: 47 | size, flag = self._find_moov_request() 48 | if flag == 'moov': 49 | time_scale, duration = self._find_duration_request() 50 | self.duration = duration / time_scale 51 | return self.duration 52 | else: 53 | self.seek += size 54 | --------------------------------------------------------------------------------