moodle-scraper 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moodle_scraper-0.1.0/.env.example +30 -0
- moodle_scraper-0.1.0/.github/workflows/ci.yml +36 -0
- moodle_scraper-0.1.0/.github/workflows/release.yml +47 -0
- moodle_scraper-0.1.0/.gitignore +30 -0
- moodle_scraper-0.1.0/LICENSE +21 -0
- moodle_scraper-0.1.0/PKG-INFO +335 -0
- moodle_scraper-0.1.0/README.md +298 -0
- moodle_scraper-0.1.0/pyproject.toml +85 -0
- moodle_scraper-0.1.0/src/moodle_scraper/__init__.py +7 -0
- moodle_scraper-0.1.0/src/moodle_scraper/__main__.py +8 -0
- moodle_scraper-0.1.0/src/moodle_scraper/cli.py +435 -0
- moodle_scraper-0.1.0/src/moodle_scraper/config.py +130 -0
- moodle_scraper-0.1.0/src/moodle_scraper/discovery.py +552 -0
- moodle_scraper-0.1.0/src/moodle_scraper/downloader.py +500 -0
- moodle_scraper-0.1.0/src/moodle_scraper/files.py +272 -0
- moodle_scraper-0.1.0/src/moodle_scraper/manifest.py +225 -0
- moodle_scraper-0.1.0/src/moodle_scraper/models.py +110 -0
- moodle_scraper-0.1.0/src/moodle_scraper/session.py +234 -0
- moodle_scraper-0.1.0/tests/conftest.py +45 -0
- moodle_scraper-0.1.0/tests/fixtures/course_named_page.html +14 -0
- moodle_scraper-0.1.0/tests/fixtures/course_page.html +59 -0
- moodle_scraper-0.1.0/tests/fixtures/folder_page.html +27 -0
- moodle_scraper-0.1.0/tests/fixtures/login_page.html +22 -0
- moodle_scraper-0.1.0/tests/fixtures/my_courses.html +25 -0
- moodle_scraper-0.1.0/tests/fixtures/resource_page.html +16 -0
- moodle_scraper-0.1.0/tests/fixtures/url_page.html +11 -0
- moodle_scraper-0.1.0/tests/test_cli.py +32 -0
- moodle_scraper-0.1.0/tests/test_discovery.py +395 -0
- moodle_scraper-0.1.0/tests/test_downloader.py +370 -0
- moodle_scraper-0.1.0/tests/test_files.py +272 -0
- moodle_scraper-0.1.0/tests/test_manifest.py +153 -0
- moodle_scraper-0.1.0/tests/test_session.py +169 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copy to `.env` and fill in. `.env` is git-ignored — never commit a real cookie.
|
|
2
|
+
#
|
|
3
|
+
# The MoodleSession cookie is a secret: anyone holding it can act as you until the
|
|
4
|
+
# session times out (8 hours on learn.haw-kiel.de). Treat it like a password.
|
|
5
|
+
|
|
6
|
+
# Base URL of the Moodle instance.
|
|
7
|
+
MOODLE_BASE_URL=https://learn.haw-kiel.de
|
|
8
|
+
|
|
9
|
+
# Option A: paste the cookie value (or a full "Cookie:" header) directly.
|
|
10
|
+
# MOODLE_COOKIE=MoodleSession=replace-me
|
|
11
|
+
|
|
12
|
+
# Option B: point at an exported Netscape cookies.txt file.
|
|
13
|
+
# MOODLE_COOKIE_FILE=./cookies.txt
|
|
14
|
+
|
|
15
|
+
# Where mirrored course files and manifest.json files are written.
|
|
16
|
+
MOODLE_OUT=data
|
|
17
|
+
|
|
18
|
+
# Politeness: seconds between requests, and parallel downloads (1 = sequential).
|
|
19
|
+
MOODLE_DELAY=0.5
|
|
20
|
+
MOODLE_CONCURRENCY=1
|
|
21
|
+
|
|
22
|
+
# Per-request timeout in seconds. Raise this if the server answers slowly
|
|
23
|
+
# (learn.haw-kiel.de dashboard pages can take well over 30 seconds).
|
|
24
|
+
MOODLE_TIMEOUT=30
|
|
25
|
+
|
|
26
|
+
# Optional byte cap per file; unset/0 means unlimited.
|
|
27
|
+
# MOODLE_MAX_FILE_SIZE=0
|
|
28
|
+
|
|
29
|
+
# Set to 1 only behind an intercepting campus proxy that breaks TLS verification.
|
|
30
|
+
MOODLE_INSECURE=0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package with dev dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Lint (ruff)
|
|
28
|
+
run: |
|
|
29
|
+
ruff check .
|
|
30
|
+
ruff format --check .
|
|
31
|
+
|
|
32
|
+
- name: Type check (mypy)
|
|
33
|
+
run: mypy src
|
|
34
|
+
|
|
35
|
+
- name: Test (pytest)
|
|
36
|
+
run: pytest -q
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Set up Python
|
|
15
|
+
uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: |
|
|
21
|
+
pip install build twine
|
|
22
|
+
python -m build
|
|
23
|
+
twine check dist/*
|
|
24
|
+
|
|
25
|
+
- name: Upload distribution artifact
|
|
26
|
+
uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
name: Publish to PyPI
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
permissions:
|
|
37
|
+
# Required for PyPI trusted publishing (OIDC) — no API token stored.
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- name: Download distribution artifact
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
- name: Publish
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
|
|
17
|
+
# Secrets and scraped content must never be committed
|
|
18
|
+
.env
|
|
19
|
+
*.session
|
|
20
|
+
*.part
|
|
21
|
+
data/
|
|
22
|
+
|
|
23
|
+
# Kilo-managed worktrees
|
|
24
|
+
.kilo/
|
|
25
|
+
|
|
26
|
+
# Editors / OS
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
Thumbs.db
|
|
30
|
+
desktop.ini
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon Schwärzler
|
|
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.
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: moodle-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mirror the course files of your learn.haw-kiel.de Moodle courses into a local, resumable folder tree.
|
|
5
|
+
Project-URL: Homepage, https://github.com/RTXC01/moodle_scrape
|
|
6
|
+
Project-URL: Repository, https://github.com/RTXC01/moodle_scrape
|
|
7
|
+
Project-URL: Issues, https://github.com/RTXC01/moodle_scrape/issues
|
|
8
|
+
Author-email: RTXC1 <audiundlambofan@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: backup,course-files,e-learning,haw-kiel,moodle,scraper
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: lxml>=5.2
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0
|
|
27
|
+
Requires-Dist: rich>=13.7
|
|
28
|
+
Requires-Dist: typer>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# moodle-scraper
|
|
39
|
+
|
|
40
|
+
[](https://github.com/RTXC01/moodle_scrape/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/moodle-scraper/)
|
|
42
|
+
[](https://pypi.org/project/moodle-scraper/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
|
|
45
|
+
Mirror the **course files** of your enrolled Moodle courses on
|
|
46
|
+
[`learn.haw-kiel.de`](https://learn.haw-kiel.de) into a local folder tree, with a per-course
|
|
47
|
+
`manifest.json` that makes repeat runs incremental and resumable.
|
|
48
|
+
|
|
49
|
+
It is a personal, read-only backup tool: it downloads the files you already have access to, and
|
|
50
|
+
never touches grades, submissions, forums, quizzes, or messages.
|
|
51
|
+
|
|
52
|
+
Built for HAW Kiel's Moodle instance, but any Moodle base URL works (`MOODLE_BASE_URL`).
|
|
53
|
+
|
|
54
|
+
## What it does
|
|
55
|
+
|
|
56
|
+
- Discovers your courses (dashboard where available, or directly by course id/URL).
|
|
57
|
+
- Walks each course's sections and activities and resolves the file links behind
|
|
58
|
+
`mod_resource`, `mod_folder`, `mod_url`, `mod_page`, `mod_book` and `mod_imscp`.
|
|
59
|
+
- Mirrors those files under `data/<shortname>-<id>/<NN>-<section>/<activity>/...`.
|
|
60
|
+
- Records size / `Last-Modified` / ETag / optional SHA-1 in `manifest.json`.
|
|
61
|
+
- On later runs it skips unchanged files, resumes interrupted `.part` downloads, and can prune
|
|
62
|
+
files that were removed upstream.
|
|
63
|
+
|
|
64
|
+
## What it does *not* do
|
|
65
|
+
|
|
66
|
+
Forums, grades, assignment submissions, quizzes, calendar, messages, streaming video/HLS capture,
|
|
67
|
+
HTML→Markdown conversion, scheduling, or any upload/POST — all out of scope. It also does not
|
|
68
|
+
automate SSO login.
|
|
69
|
+
|
|
70
|
+
## Legal and ethical use
|
|
71
|
+
|
|
72
|
+
- Only use this for content you are **already enrolled in**, as a **personal backup**. Do not
|
|
73
|
+
redistribute any downloaded material.
|
|
74
|
+
- The tool only issues `GET`/`HEAD` requests. It never posts, submits, or grades anything.
|
|
75
|
+
- Defaults are deliberately polite: sequential requests, a `0.5s` delay, one download at a time,
|
|
76
|
+
exponential backoff on `429`/`5xx`, and an identifying `User-Agent`. Please keep the delay unless
|
|
77
|
+
you have a reason not to.
|
|
78
|
+
- Your `MoodleSession` cookie is a secret. It is read from `.env`/environment/a cookie file, never
|
|
79
|
+
logged, and never written into a manifest. `.env` and `data/` are git-ignored.
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
- Python **3.11+**
|
|
84
|
+
- A browser where you are logged in to your Moodle instance.
|
|
85
|
+
|
|
86
|
+
## Install
|
|
87
|
+
|
|
88
|
+
From PyPI:
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
pip install moodle-scraper
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
From source:
|
|
95
|
+
|
|
96
|
+
```powershell
|
|
97
|
+
git clone https://github.com/RTXC01/moodle_scrape.git moodle_scrape
|
|
98
|
+
cd moodle_scrape
|
|
99
|
+
python -m venv .venv
|
|
100
|
+
.\.venv\Scripts\Activate.ps1
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then copy the example config and fill in your cookie:
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
Copy-Item .env.example .env
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Cookie export
|
|
111
|
+
|
|
112
|
+
The tool reuses the browser session you already have; it never asks for your password and does not
|
|
113
|
+
drive SSO. Export the `MoodleSession` cookie and put it in `.env` as `MOODLE_COOKIE` (or save a
|
|
114
|
+
Netscape `cookies.txt` and point `MOODLE_COOKIE_FILE` at it).
|
|
115
|
+
|
|
116
|
+
### Chrome / Edge
|
|
117
|
+
|
|
118
|
+
1. Log in at <https://learn.haw-kiel.de>.
|
|
119
|
+
2. Open DevTools (`F12`) → **Application** → **Storage** → **Cookies** →
|
|
120
|
+
`https://learn.haw-kiel.de`.
|
|
121
|
+
3. Copy the **Value** of the `MoodleSession` cookie.
|
|
122
|
+
4. Put it in `.env`:
|
|
123
|
+
|
|
124
|
+
```dotenv
|
|
125
|
+
MOODLE_COOKIE=MoodleSession=<paste-value>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A bare value also works (`MOODLE_COOKIE=<paste-value>`), as does a full header
|
|
129
|
+
(`MOODLE_COOKIE=Cookie: MoodleSession=...`).
|
|
130
|
+
|
|
131
|
+
### Firefox
|
|
132
|
+
|
|
133
|
+
1. Log in at <https://learn.haw-kiel.de>.
|
|
134
|
+
2. DevTools (`F12`) → **Storage** → **Cookies** → `https://learn.haw-kiel.de` → copy the
|
|
135
|
+
`MoodleSession` **Value**.
|
|
136
|
+
3. Paste it into `.env` as above. Alternatively, use a "cookies.txt" export extension, save the
|
|
137
|
+
Netscape file, and set `MOODLE_COOKIE_FILE=./cookies.txt`.
|
|
138
|
+
|
|
139
|
+
> The Moodle session times out after ~8 hours (`sessiontimeout: 28800`). When it expires, commands
|
|
140
|
+
> exit with code `3` and tell you to re-export the cookie.
|
|
141
|
+
|
|
142
|
+
## Commands
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
moodle-scrape doctor
|
|
146
|
+
moodle-scrape courses
|
|
147
|
+
moodle-scrape files <course>
|
|
148
|
+
moodle-scrape sync [--course ID]... [options]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `doctor`
|
|
152
|
+
|
|
153
|
+
Validates the cookie, prints the logged-in account and how many courses are visible.
|
|
154
|
+
Exit code `0` when the session is good, `3` when it is expired or invalid.
|
|
155
|
+
|
|
156
|
+
```powershell
|
|
157
|
+
moodle-scrape doctor
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### `courses`
|
|
161
|
+
|
|
162
|
+
Prints a table of enrolled courses (id, shortname, fullname, discovered file count).
|
|
163
|
+
|
|
164
|
+
```powershell
|
|
165
|
+
moodle-scrape courses
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `files`
|
|
169
|
+
|
|
170
|
+
Dry discovery for one course: lists the files that *would* be downloaded, without downloading.
|
|
171
|
+
|
|
172
|
+
```powershell
|
|
173
|
+
moodle-scrape files 12345
|
|
174
|
+
moodle-scrape files https://learn.haw-kiel.de/course/view.php?id=12345
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `sync`
|
|
178
|
+
|
|
179
|
+
The main command.
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
--course ID|URL Repeatable. Defaults to all enrolled courses.
|
|
183
|
+
--out DIR Output directory (default: data).
|
|
184
|
+
--delay SECONDS Delay between requests (default: 0.5).
|
|
185
|
+
--timeout SECONDS Per-request timeout (default: 30; raise it for slow servers).
|
|
186
|
+
--concurrency N Parallel downloads (default: 1 = sequential).
|
|
187
|
+
--dry-run Resolve and report everything, download nothing.
|
|
188
|
+
--prune Delete local files that no longer exist upstream.
|
|
189
|
+
--checksum Compute SHA-1 while downloading (slower).
|
|
190
|
+
--max-file-size N Skip files larger than N bytes (default: unlimited).
|
|
191
|
+
--insecure Disable TLS verification (only behind a broken campus proxy).
|
|
192
|
+
--verbose Verbose logging; the cookie is always redacted.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
|
|
197
|
+
```powershell
|
|
198
|
+
# Everything, politely
|
|
199
|
+
moodle-scrape sync
|
|
200
|
+
|
|
201
|
+
# One course, preview only
|
|
202
|
+
moodle-scrape sync --course 12345 --dry-run
|
|
203
|
+
|
|
204
|
+
# Two courses, checksums, prune removed files
|
|
205
|
+
moodle-scrape sync --course 12345 --course 67890 --checksum --prune
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> **Slow dashboard?** Some Moodle instances answer very slowly under load. The tool retries
|
|
209
|
+
> transient login bounces and transport errors automatically; for this site a timeout of
|
|
210
|
+
> `MOODLE_TIMEOUT=180` in `.env` is a good idea. Single-course runs fetch the course page directly
|
|
211
|
+
> and skip the dashboard entirely.
|
|
212
|
+
|
|
213
|
+
## Output layout
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
data/
|
|
217
|
+
Analysis-12345/ # <shortname>-<id>
|
|
218
|
+
00-General/
|
|
219
|
+
Slides.pdf
|
|
220
|
+
Scripts/
|
|
221
|
+
chapter1.pdf # nested folder structure preserved
|
|
222
|
+
01-Week 1/
|
|
223
|
+
Exercises/
|
|
224
|
+
Notes.txt
|
|
225
|
+
_external/ # recorded links, not downloaded
|
|
226
|
+
manifest.json
|
|
227
|
+
Physics-67890/
|
|
228
|
+
...
|
|
229
|
+
manifest.json
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Filenames and directories are sanitised for Windows (`<>:"/\|?*`, control characters, trailing
|
|
233
|
+
dots/spaces, reserved device names such as `CON`/`PRN`, over-long components, and path-length
|
|
234
|
+
limits). If two different URLs would map to the same path, a short deterministic hash
|
|
235
|
+
(`__<8 hex chars>`) is inserted before the extension.
|
|
236
|
+
|
|
237
|
+
## Manifest format
|
|
238
|
+
|
|
239
|
+
One `manifest.json` per course, written atomically after the course finishes (and rewritten after
|
|
240
|
+
partial failures so a rerun resumes instead of restarting):
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"schema_version": 1,
|
|
245
|
+
"course": { "id": 12345, "shortname": "Analysis", "fullname": "Analysis I" },
|
|
246
|
+
"generated_at": "2026-09-15T19:30:00+00:00",
|
|
247
|
+
"files": [
|
|
248
|
+
{
|
|
249
|
+
"rel_path": "Analysis-12345/00-General/Slides.pdf",
|
|
250
|
+
"url": "https://learn.haw-kiel.de/pluginfile.php/1/mod_resource/content/0/Slides.pdf",
|
|
251
|
+
"section": "General",
|
|
252
|
+
"activity": "",
|
|
253
|
+
"filename": "Slides.pdf",
|
|
254
|
+
"size": 1048576,
|
|
255
|
+
"last_modified": "Wed, 03 Sep 2026 10:00:00 GMT",
|
|
256
|
+
"etag": "\"abc123\"",
|
|
257
|
+
"sha1": null,
|
|
258
|
+
"downloaded": true,
|
|
259
|
+
"external": false,
|
|
260
|
+
"status": "downloaded",
|
|
261
|
+
"fetched_at": "2026-09-15T19:31:00+00:00"
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`status` is one of `pending`, `downloaded`, `skipped`, `missing`, `failed`, `external`,
|
|
268
|
+
`unsupported`. `external: true` entries are links pointing outside the Moodle host; they are
|
|
269
|
+
recorded, never fetched.
|
|
270
|
+
|
|
271
|
+
## Exit codes
|
|
272
|
+
|
|
273
|
+
| Code | Meaning |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `0` | Success |
|
|
276
|
+
| `1` | Unexpected error |
|
|
277
|
+
| `2` | Bad usage or configuration (no cookie, bad values, missing cookie file) |
|
|
278
|
+
| `3` | Session expired or invalid |
|
|
279
|
+
|
|
280
|
+
## Incremental behaviour
|
|
281
|
+
|
|
282
|
+
1. If the manifest entry for a URL matches the local file's size (and, when available,
|
|
283
|
+
`Last-Modified`/ETag via a conditional request), the file is **skipped**.
|
|
284
|
+
2. Otherwise the file is downloaded to `<name>.part`, flushed, then atomically renamed. If the run
|
|
285
|
+
is interrupted, the `.part` file stays and the next run resumes it with a `Range` request when
|
|
286
|
+
the server supports it.
|
|
287
|
+
3. `404` is recorded as `missing` and the run continues. `401`/`403` triggers a session check and
|
|
288
|
+
aborts the run.
|
|
289
|
+
|
|
290
|
+
## Development
|
|
291
|
+
|
|
292
|
+
```powershell
|
|
293
|
+
pip install -e ".[dev]"
|
|
294
|
+
ruff check .
|
|
295
|
+
ruff format --check .
|
|
296
|
+
mypy src
|
|
297
|
+
pytest -q
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Tests are fully offline: HTML fixtures under `tests/fixtures/` plus `httpx.MockTransport` cover
|
|
301
|
+
discovery, path sanitisation, manifest diffing/pruning, cookie parsing, session-expiry detection
|
|
302
|
+
and retry/backoff behaviour.
|
|
303
|
+
|
|
304
|
+
### Releasing (maintainers)
|
|
305
|
+
|
|
306
|
+
1. Bump `__version__` in `src/moodle_scraper/__init__.py`.
|
|
307
|
+
2. Commit and tag: `git tag v0.1.0 && git push origin v0.1.0` — the
|
|
308
|
+
[release workflow](.github/workflows/release.yml) builds the sdist/wheel, runs `twine check`,
|
|
309
|
+
and publishes to PyPI via [trusted publishing](https://docs.pypi.org/trusted-publishers/)
|
|
310
|
+
(configure the PyPI project once: owner `RTXC01`, repository `moodle_scrape`, workflow
|
|
311
|
+
`release.yml`, environment `pypi`).
|
|
312
|
+
3. Manual alternative: `python -m build && twine upload dist/*`.
|
|
313
|
+
|
|
314
|
+
## Troubleshooting
|
|
315
|
+
|
|
316
|
+
- **Exit code 3 / "session expired"** — log in again in your browser and re-export the cookie.
|
|
317
|
+
- **A course page returns no files** — Moodle markup varies by version/theme. Run
|
|
318
|
+
`moodle-scrape files <id> --verbose` and check whether the activity selectors in
|
|
319
|
+
`discovery.py` match. All selectors live in that one module.
|
|
320
|
+
- **A single activity is skipped** — some activities bounce to login even with a valid session
|
|
321
|
+
(insufficient rights); they are skipped individually instead of aborting the run. Run with
|
|
322
|
+
`--verbose` to see which.
|
|
323
|
+
- **Files look larger than expected / slow** — use `--max-file-size` to skip big video files, and
|
|
324
|
+
keep `--concurrency 1`.
|
|
325
|
+
- **TLS errors behind a campus proxy** — `--insecure` disables verification for that run only.
|
|
326
|
+
|
|
327
|
+
## Roadmap
|
|
328
|
+
|
|
329
|
+
- `browser_cookie3` browser-store extraction as an optional extra.
|
|
330
|
+
- Optionally record `mod_page`/`mod_book` text content in the manifest (today only their
|
|
331
|
+
attachments are collected).
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
[MIT](LICENSE)
|