PKJcN_ VVe3dump/__init__.py"""e3dump - dump your NCTU e3 course materials by one click""" __version__ = '1.0.0' PK`NPDž[[e3dump/__main__.py# -*- coding: utf-8 -*- from e3dump.cli import run if __name__ == '__main__': run() PKTNIRe3dump/auth.py# -*- coding: utf-8 -*- from lxml import etree from e3dump.utils import E3_LOGIN_URL async def login(client, username, password): data = {'username': username, 'password': password} async with client.post(E3_LOGIN_URL, data=data) as resp: assert resp.status == 200 text = await resp.text() root = etree.HTML(text) return ':' not in root.xpath('//title/text()')[0], root PK bN8j e3dump/cli.py# -*- coding: utf-8 -*- import argparse import asyncio from e3dump.main import main def get_parser(): parser = argparse.ArgumentParser(prog='e3dump', description='NCTU e3 dump') parser.add_argument('--username', type=str, help='NCTU e3 username', required=True) parser.add_argument('--password', type=str, help='NCTU e3 password', required=True) parser.add_argument( '--path', type=str, default='.', help='Dump to this path, default is current pwd') return parser def run(): args = get_parser().parse_args() loop = asyncio.get_event_loop() loop.run_until_complete(main(args.username, args.password, args.path)) PKH`N|) ) e3dump/fetcher.py# -*- coding: utf-8 -*- import asyncio import pathlib from lxml import etree from e3dump.utils import E3_MATERIAL_URL from e3dump.utils import logger CHUNK_SIZE = 4 * 1024 def get_course_id(url): return url.split('=')[-1] def get_current_courses_list(root): student = list(map(get_course_id, root.xpath( 'id("layer2_right_current_course_stu")//a[@class="course-link"]/@href'))) ta = list(map(get_course_id, root.xpath( 'id("layer2_right_current_course_tea")//a[@class="course-link"]/@href'))) return student, ta async def get_course_materials(client, course_id): async with client.get(E3_MATERIAL_URL.format(course_id=course_id)) as resp: assert resp.status == 200 text = await resp.text() root = etree.HTML(text) course_name = root.xpath( '//h1[1]/text()')[0].replace('/', '-').replace('\\', '-') materials = [] for row in root.xpath('//tr'): if not row.xpath('td'): continue title = ''.join(row.xpath('td[1]')[0].itertext()).strip( '\n ').replace('/', '-').replace('\\', '-') material = [(i.text.strip('\n ').replace('/', '-').replace('\\', '-'), i.get('href')) for i in row.xpath('td[2]//a')] materials.append((title, material)) return course_name, materials async def download_file(client, url, path): async with client.get(url) as resp: assert resp.status == 200 with open(path, 'wb') as f: while True: chunk = await resp.content.read(CHUNK_SIZE) if not chunk: break f.write(chunk) return path.name, url async def download_materials(client, materials, base_path, course_name): futures = [] base_path = pathlib.Path(base_path) for index, value in enumerate(materials): folder, material = value folder = base_path / course_name / f'{index:02d} - {folder}' folder.mkdir(parents=True, exist_ok=True) for filename, url in material: path = folder / filename futures.append(download_file(client, url, path)) for f in asyncio.as_completed(futures): filename, url = await f logger.info(f'Download finished: {filename}, {url}') PK`NΈae3dump/main.py# -*- coding: utf-8 -*- import asyncio import aiohttp from e3dump.auth import login from e3dump.fetcher import get_current_courses_list from e3dump.fetcher import get_course_materials from e3dump.fetcher import download_materials from e3dump.utils import logger async def main(username, password, base_path): async with aiohttp.ClientSession() as client: success, root = await login(client, username, password) if not success: logger.critical('Please check your username or password again') return logger.info('NCTU e3 login successfully') student, ta = get_current_courses_list(root) for course_id in student + ta: course_name, materials = await get_course_materials(client, course_id) logger.info(f'Start: {course_name}') await download_materials(client, materials, base_path, course_name) logger.info('Done!') if __name__ == '__main__': loop = asyncio.get_event_loop() root = loop.run_until_complete(main()) PK`NIle3dump/utils.py# -*- coding: utf-8 -*- import colorlog E3_LOGIN_URL = 'https://e3new.nctu.edu.tw/login/index.php' E3_MATERIAL_URL = 'https://e3new.nctu.edu.tw/local/courseextension/index.php?courseid={course_id}' # Logger handler = colorlog.StreamHandler() formatter = colorlog.ColoredFormatter( "%(log_color)s%(levelname)-8s%(reset)s %(message)s", datefmt=None, reset=True, log_colors={ 'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red,bg_white', }, secondary_log_colors={}, style='%' ) handler.setFormatter(formatter) logger = colorlog.getLogger('e3dump') logger.addHandler(handler) logger.setLevel('INFO') PK!HާO&)'e3dump-1.0.0.dist-info/entry_points.txtN+I/N.,()J5N)-Pz9VEy\\PKcNSe3dump-1.0.0.dist-info/LICENSECopyright 2019 Louie Lu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H>*RQe3dump-1.0.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UrPK!H ] e3dump-1.0.0.dist-info/METADATAmN1IhDX^n"h^M;'g~Qa|ЮpǬBKh؏2#>bd-,Cһ䡚mwM k4=@-٣TtXM]eYؤ=Ζָɤ4~%!{wSz4ytYF@;Fp3}$)>{ 7~fCo|V9LλORCooҽQj7VmR6Kn:q5[5= m PK!H0te3dump-1.0.0.dist-info/RECORDuɒ@{? a(l j(ǎ&/3!ޫW(l'j7,j30x6EwxԳz`6BD>UAyWھgKe;zI Mw%w5]Z! 8AB1|XY8=K\V31PK B7)/V=W4܉һo2o"6zA Nrؽ@jGkVYiR|(GTn-NK QsL&'8x߁.Uof0ʅVI7Xjap84sg Q XGvlRIg./CҟsRc_/Xnո#^?^z%%B+g42Ti` N'y|;Jn dU~^W| &B!ܪOK,T3i]YZtE]D6J "j#DG\@foENNgMlx<*@TF{%#?PKJcN_ VVe3dump/__init__.pyPK`NPDž[[e3dump/__main__.pyPKTNIRe3dump/auth.pyPK bN8j e3dump/cli.pyPKH`N|) ) e3dump/fetcher.pyPK`NΈa)e3dump/main.pyPK`NIlee3dump/utils.pyPK!HާO&)'`e3dump-1.0.0.dist-info/entry_points.txtPKcNSe3dump-1.0.0.dist-info/LICENSEPK!H>*RQe3dump-1.0.0.dist-info/WHEELPK!H ] e3dump-1.0.0.dist-info/METADATAPK!H0te3dump-1.0.0.dist-info/RECORDPK 2^