Skip to content

ghosty2004/moodle-dl-web

Repository files navigation

moodle-dl-web

Self-hosted web UI that mirrors files from any Moodle instance to local disk, on a schedule. Configure per-course file-type filters, watch sync runs live, and optionally keep a Kavita book library in sync via hardlinks.

What it does

  • Connects to one or more Moodle sites using the mobile Web Services API (username/password → long-lived token).
  • Lists your enrolled courses and lets you toggle which ones to sync.
  • Per-course filters: file-type categories (PDFs, Office docs, images, video, etc.) and filename regex.
  • Downloads files into a tree that mirrors Moodle's sections/modules: data/account-<id>/<course>/<section>/<module>/<filename>.
  • Re-syncs are incremental and survive rename of sections/modules (files get moved, not redownloaded).
  • Scheduled runs via cron expression, or trigger one on demand.
  • Live progress over WebSocket: stats, per-file events, error logs.
  • Optional Kavita mirror: rebuilds a second tree under a configurable path using hardlinks (no extra disk usage). Auto-runs after each sync and/or after a course/account delete.

Stack

  • Server: NestJS 11, better-sqlite3 + Drizzle ORM, @nestjs/schedule for cron, @nestjs/platform-ws for the live stream.
  • Client: React 19 + Vite 6, Tailwind v4, React Router 7, Lucide icons, Sonner toasts.
  • Infra: pnpm workspace + Turbo. Dev devcontainer with traefik + mkcert-signed local TLS.

Running in production

A single image bundles the server and the built client. The server serves the SPA at /, the REST API at /api/*, and the WebSocket at /ws/*.

docker run --rm -p 3000:3000 \
  -v $(pwd)/data:/data \
  -v $(pwd)/kavita-library:/kavita-library \
  -e SYNC_INTERVAL_CRON="0 * * * *" \
  ghcr.io/ghosty2004/moodle-dl-web:latest

Open http://localhost:3000. The /data volume holds the SQLite database + downloaded files. /kavita-library is optional and only used when the Kavita mirror is enabled in Settings.

docker-compose

services:
  moodle-dl-web:
    image: ghcr.io/ghosty2004/moodle-dl-web:latest
    container_name: moodle-dl-web
    ports:
      - "3000:3000"
    volumes:
      - ./data:/data
      - ./kavita-library:/kavita-library
    environment:
      SYNC_INTERVAL_CRON: "0 * * * *"
    restart: unless-stopped

Save as docker-compose.yaml and run docker compose up -d. Logs: docker compose logs -f.

Environment variables:

Var Required Description
SYNC_INTERVAL_CRON no 5-field cron, e.g. 0 * * * *. Empty disables the scheduler
CLIENT_URL no Only needed if you serve the client from a different origin than the API (enables CORS for that origin)
API_PREFIX no HTTP prefix for API routes (set to api in the shipped image)
SERVE_STATIC no true to serve the client bundle from ./public

Development

The project is set up around a VS Code devcontainer.

cp .devcontainer/.env.example .devcontainer/.env

Then "Reopen in Container". Inside:

pnpm run dev          # runs server (3000) and client (5173) via turbo

Traefik exposes the apps over HTTPS on:

  • https://moodle-dl-web.localdev — client (Vite dev server)
  • https://api.moodle-dl-web.localdev — server REST + WebSocket

Add both hostnames to your host's /etc/hosts pointing at 127.0.0.1.

Drizzle migrations

Schema lives in apps/server/src/db/schema.ts. After any change:

cd apps/server
pnpm run db:generate   # emits a new SQL file under drizzle/

Migrations are applied automatically on server boot.

CI

.github/workflows/publish.yml builds the single production image for linux/amd64 + linux/arm64 and pushes to GHCR on:

  • push to main → tag main, sha-<short>, latest
  • push of v*.*.* tag → semver tags

License

MIT

About

Self-hosted Moodle scraper with a web UI — scheduled syncs, per-course filters, optional Kavita mirror. Ships as a single Docker image.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors