├── .flake8
├── .github
├── ISSUE_TEMPLATE
│ └── feature_request.md
└── workflows
│ ├── geektime-dl-ci.yml
│ └── publish.yml
├── .gitignore
├── Dockerfile
├── README.md
├── docs
├── .gitignore
├── .vuepress
│ ├── components
│ │ ├── PostCard.vue
│ │ └── PostList.vue
│ ├── config.js
│ ├── enhanceApp.js
│ └── public
│ │ ├── Wechat.jpeg
│ │ ├── conglingkaishixuejiagou.jpeg
│ │ ├── favicon.jpg
│ │ ├── geektime.gif
│ │ ├── qiuyuedechanpinshouji.jpeg
│ │ ├── rengongzhinengjichuke.jpeg
│ │ ├── tuijianxitong36shi.jpeg
│ │ ├── zhuyundejishuguanlike.jpeg
│ │ └── zuoertingfeng.jpeg
├── README.md
├── bonus.md
├── faq.md
├── geektime_data.js
├── guide.md
├── intro.md
├── package-lock.json
├── package.json
├── recruit.md
└── tldr.md
├── geektime.py
├── geektime_dl
├── __init__.py
├── cache.py
├── cli
│ ├── __init__.py
│ ├── command.py
│ ├── ebook.py
│ ├── login.py
│ └── query.py
├── dal.py
├── ebook
│ ├── __init__.py
│ ├── ebook.py
│ └── templates
│ │ └── article.html
├── gt_apis.py
├── log.py
└── utils.py
├── requirements
├── base.txt
└── dev.txt
├── setup.py
└── tests
├── conftest.py
├── test_cli
├── test_basic.py
├── test_ebook.py
└── test_query.py
├── test_ebook_util.py
├── test_gt_apis.py
└── test_utils.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | count = True
3 | max-complexity = 10
4 | max-line-length = 80
5 | statistics = True
6 | ignore = W391, W503, W504
7 | exclude =
8 | __pycache__
9 | venv
10 | .venv
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/geektime-dl-ci.yml:
--------------------------------------------------------------------------------
1 | name: CI & CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | max-parallel: 1
13 | matrix:
14 | python-version: [3.7]
15 |
16 | steps:
17 | - uses: actions/checkout@v1
18 | - name: Set up Python ${{ matrix.python-version }}
19 | uses: actions/setup-python@v1
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip wheel
25 | pip install -r requirements/base.txt
26 | - name: Lint with flake8
27 | run: |
28 | pip install flake8
29 | flake8
30 | - name: Test with pytest
31 | env:
32 | account: ${{ secrets.account }}
33 | password: ${{ secrets.password }}
34 | run: |
35 | pip install -r requirements/dev.txt
36 | python -m pytest
37 | - name: Coverage
38 | run: |
39 | pip install coverage coveralls
40 | coverage run --source=geektime_dl -m pytest tests/
41 | coveralls --service=github
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 |
45 | docs:
46 | runs-on: ubuntu-latest
47 |
48 | steps:
49 | - name: Checkout master
50 | uses: actions/checkout@v2
51 | with:
52 | ref: master
53 |
54 | - name: Setup node
55 | uses: actions/setup-node@v1
56 | with:
57 | node-version: "12.x"
58 |
59 | - name: Build project
60 | run: |
61 | cd docs
62 | npm install
63 | npm run docs:build
64 |
65 | - name: Upload gh-pages
66 | uses: peaceiris/actions-gh-pages@v3
67 | with:
68 | github_token: ${{ secrets.GITHUB_TOKEN }}
69 | publish_dir: ./docs/.vuepress/dist
70 |
71 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # This workflows will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v1
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install setuptools wheel twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
29 | run: |
30 | python setup.py sdist bdist_wheel
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | *.log
3 | .DS_Store
4 |
5 | .cache
6 | .pytest_cache
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Packages
12 | *.egg
13 | *.egg-info
14 | dist
15 | build
16 | eggs
17 | parts
18 | bin
19 | var
20 | sdist
21 | develop-eggs
22 | .installed.cfg
23 | lib
24 | lib64
25 | __pycache__
26 |
27 | # Installer logs
28 | pip-log.txt
29 |
30 | # Unit test / coverage reports
31 | .coverage
32 | .tox
33 | nosetests.xml
34 |
35 |
36 | # Virtual environment
37 | .venv
38 | .venv3
39 | venv
40 |
41 | # Environment files
42 | .idea
43 |
44 | # tmp files
45 | geektime.cfg
46 | htmlcov
47 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7-alpine
2 |
3 | RUN apk add --no-cache jpeg-dev zlib-dev
4 | RUN apk add --no-cache --virtual .build-deps build-base linux-headers \
5 | && pip install Pillow
6 | RUN pip install -U geektime_dl
7 |
8 | WORKDIR /output
9 |
10 | ENTRYPOINT ["geektime"]
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | :sparkles: 重要 :sparkles:
3 |
4 | **查看 [使用文档](https://jachinlin.github.io/geektime_dl/) 获取最新使用信息。**
5 |
6 |
7 |
8 | 本 README.md 不再更新!:point_down:
9 |
10 |
11 |
12 |
15 | {{ post.summary }} 16 | 17 | {{ post.readMoreText || 'Read more'}} 18 | 19 |
20 |hello world
' 12 | render.render_article_html(title, content) 13 | fn = os.path.join(output_folder, title + '.html') 14 | 15 | assert os.path.isfile(fn) 16 | with open(fn) as f: 17 | assert content in f.read() 18 | 19 | os.remove(fn) 20 | 21 | 22 | def test_render_toc_md(render: Render, output_folder: str): 23 | title = 'hello' 24 | headers = ['标题1', '标题2'] 25 | render.render_toc_md(title, headers) 26 | fn = os.path.join(output_folder, 'toc.md') 27 | 28 | assert os.path.isfile(fn) 29 | with open(fn) as f: 30 | ls = f.readlines() 31 | assert len(ls) == 3 32 | assert ls[0].rstrip('\n') == title 33 | assert ls[1].rstrip('\n') == '# {}'.format(headers[0]) 34 | assert ls[2].rstrip('\n') == '# {}'.format(headers[1]) 35 | 36 | os.remove(fn) 37 | 38 | -------------------------------------------------------------------------------- /tests/test_gt_apis.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | from geektime_dl.gt_apis import GkApiClient 4 | 5 | course_keys_needed = { 6 | 'id', 'column_title', 'had_sub', 'is_finish', 'update_frequency' 7 | } 8 | post_keys_needed = { 9 | 'id', 'article_title', 'article_content', 'column_id' 10 | } 11 | comment_keys_needed = { 12 | 'user_name', 'like_count', 'comment_content', 'comment_ctime' 13 | } 14 | daily_video_keys_needed = { 15 | 'id', 'article_title', 'column_had_sub', 'video_media_map' 16 | } 17 | 18 | video_id = 2184 19 | collection_id = 141 20 | daily_id = 113850 21 | 22 | 23 | # def test_api_get_course_list(gk: GkApiClient): 24 | # res = gk.get_course_list() 25 | # 26 | # assert isinstance(res, dict) 27 | # assert {'1', '2', '3', '4'} & set(res.keys()) 28 | # for type_ in {'1', '2', '3', '4'}: 29 | # course_list = res[type_]['list'] 30 | # course = course_list[0] 31 | # assert isinstance(course, dict) 32 | # for key in course_keys_needed: 33 | # assert course.get(key) is not None, '{} 不存在'.format(key) 34 | 35 | 36 | def test_api_get_course_intro(gk: GkApiClient, column_id): 37 | course = gk.get_course_intro(column_id) 38 | assert isinstance(course, dict) 39 | for key in course_keys_needed: 40 | assert course.get(key) is not None, '{} 不存在'.format(key) 41 | 42 | 43 | def test_api_get_course_post_list(gk: GkApiClient, column_id): 44 | course = gk.get_post_list_of(column_id) 45 | assert course and isinstance(course, list) 46 | article = course[0] 47 | for key in {'id'}: 48 | assert article.get(key) is not None, '{} 不存在'.format(key) 49 | 50 | 51 | def test_api_get_post_content(gk: GkApiClient, article_id): 52 | article = gk.get_post_content(article_id) 53 | assert article and isinstance(article, dict) 54 | for key in post_keys_needed: 55 | assert article.get(key) is not None, '{} 不存在'.format(key) 56 | 57 | # mp3 58 | assert article.get('audio_download_url') 59 | # mp4 60 | article = gk.get_post_content(video_id) 61 | vm = article.get('video_media_map') 62 | assert vm, 'video_media_map 不存在' 63 | assert vm['sd']['url'] 64 | assert vm['hd']['url'] 65 | 66 | 67 | def test_api_get_post_comments(gk: GkApiClient, article_id): 68 | res = gk.get_post_comments(article_id) 69 | assert res and isinstance(res, list) 70 | comment = res[0] 71 | for key in comment_keys_needed: 72 | assert comment.get(key) is not None, '{} 不存在'.format(key) 73 | 74 | 75 | def test_api_get_video_collection_intro(gk: GkApiClient): 76 | course = gk.get_video_collection_intro(collection_id) 77 | assert isinstance(course, dict) 78 | for key in {'cid', 'title'}: 79 | assert course.get(key) is not None, '{} 不存在'.format(key) 80 | 81 | 82 | def test_api_get_video_collection_list(gk: GkApiClient): 83 | col_list = gk.get_video_collection_list() 84 | assert col_list and isinstance(col_list, list) 85 | col = col_list[0] 86 | for key in {'collection_id'}: 87 | assert col.get(key) is not None, '{} 不存在'.format(key) 88 | 89 | 90 | # def test_api_get_collection_video_list(gk: GkApiClient): 91 | # v_list = gk.get_video_list_of(collection_id) 92 | # assert v_list and isinstance(v_list, list) 93 | # video = v_list[0] 94 | # for key in {'article_id', 'is_sub'}: 95 | # assert video.get(key) is not None, '{} 不存在'.format(key) 96 | 97 | 98 | def test_api_get_vedio_content(gk: GkApiClient): 99 | video = gk.get_post_content(daily_id) 100 | assert video and isinstance(video, dict) 101 | for key in daily_video_keys_needed: 102 | assert video.get(key) is not None, '{} 不存在'.format(key) 103 | 104 | # video_url 105 | assert 'video_media_map' in video 106 | # assert vm['sd']['url'] 107 | # assert vm['hd']['url'] 108 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | 3 | import threading 4 | import time 5 | 6 | from geektime_dl.utils import ( 7 | get_working_folder, 8 | Singleton, 9 | synchronized, 10 | parse_column_ids 11 | ) 12 | from geektime_dl import log 13 | 14 | 15 | def test_logging(): 16 | log.logger.info('guess where i will be ') 17 | 18 | log_file = get_working_folder() / 'geektime.log' 19 | with open(log_file) as f: 20 | logs = f.read() 21 | assert 'guess where i will be ' in logs 22 | assert 'INFO' in logs 23 | 24 | 25 | def test_singleton(): 26 | class S(metaclass=Singleton): 27 | pass 28 | 29 | a = S() 30 | b = S() 31 | assert a is b 32 | 33 | 34 | def test_synchronized(): 35 | 36 | class A(object): 37 | def __init__(self): 38 | self._lock = threading.Lock() 39 | 40 | def func(self): 41 | time.sleep(0.2) 42 | 43 | @synchronized() 44 | def synchronized_func(self): 45 | time.sleep(0.2) 46 | 47 | a = A() 48 | 49 | def time_cost(func) -> float: 50 | start = time.time() 51 | t_list = [] 52 | for i in range(2): 53 | t = threading.Thread(target=func) 54 | t_list.append(t) 55 | t.start() 56 | for t in t_list: 57 | t.join() 58 | return time.time() - start 59 | 60 | assert time_cost(a.synchronized_func) >= 0.2 * 2 61 | assert time_cost(a.func) < 0.2 * 2 62 | 63 | 64 | def test_parse_column_ids(): 65 | ids = '1' 66 | ids2 = '1-3' 67 | ids3 = '3,6-8' 68 | assert parse_column_ids(ids) == [1] 69 | assert parse_column_ids(ids2) == [1, 2, 3] 70 | assert parse_column_ids(ids3) == [3, 6, 7, 8] 71 | --------------------------------------------------------------------------------