Compare commits
5 Commits
main
...
auto-Push-
| Author | SHA1 | Date | |
|---|---|---|---|
| b83d814c7c | |||
| 287506f8bb | |||
| be2ee3fea7 | |||
| 7a08a63db3 | |||
| a29bceda4e |
143
index.js
143
index.js
@@ -1,17 +1,13 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
const express = require('express')
|
|
||||||
const luxon = require('luxon');
|
const luxon = require('luxon');
|
||||||
const { WebUntis } = require('webuntis');
|
const { WebUntis } = require('webuntis');
|
||||||
|
const PocketBase = require('pocketbase/cjs');
|
||||||
|
|
||||||
const app = express()
|
const pb = new PocketBase('https://fsae41.de');
|
||||||
const port = process.env.PORT;
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
async function getTimetabe() {
|
||||||
res.send('server is running')
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get('/timetable', async (req, res) => {
|
|
||||||
const untis = new WebUntis(process.env.WEBUNTIS_SCHOOL, process.env.WEBUNTIS_USER, process.env.WEBUNTIS_PASS, process.env.WEBUNTIS_URL);
|
const untis = new WebUntis(process.env.WEBUNTIS_SCHOOL, process.env.WEBUNTIS_USER, process.env.WEBUNTIS_PASS, process.env.WEBUNTIS_URL);
|
||||||
|
let timetable;
|
||||||
try {
|
try {
|
||||||
await untis.login();
|
await untis.login();
|
||||||
// Start und Ende der aktuellen Woche bestimmen
|
// Start und Ende der aktuellen Woche bestimmen
|
||||||
@@ -21,33 +17,130 @@ app.get('/timetable', async (req, res) => {
|
|||||||
const endOfWeek = now.endOf('week').plus({ days: 7 * 4 * 4 }).toJSDate(); // Sonntag
|
const endOfWeek = now.endOf('week').plus({ days: 7 * 4 * 4 }).toJSDate(); // Sonntag
|
||||||
|
|
||||||
// Stundenplan für diese Woche abrufen
|
// Stundenplan für diese Woche abrufen
|
||||||
const timetable = await untis.getOwnTimetableForRange(startOfWeek, endOfWeek);
|
timetable = await untis.getOwnTimetableForRange(startOfWeek, endOfWeek);
|
||||||
|
|
||||||
await untis.logout();
|
await untis.logout();
|
||||||
res.json({ status: 'success', data: timetable });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
res.statusCode = 500;
|
|
||||||
res.json({ status: 'error', message: error.message || error.toString() });
|
|
||||||
}
|
}
|
||||||
})
|
return timetable;
|
||||||
|
}
|
||||||
|
|
||||||
const os = require('os');
|
async function main() {
|
||||||
const nets = os.networkInterfaces();
|
const timetable = await getTimetabe();
|
||||||
const results = {};
|
|
||||||
|
|
||||||
for (const name of Object.keys(nets)) {
|
if (!timetable || timetable.length === 0) {
|
||||||
for (const net of nets[name]) {
|
console.log("No timetable data available.");
|
||||||
if (!net.internal) {
|
return;
|
||||||
results[name] = results[name] || [];
|
}
|
||||||
results[name].push(net.address);
|
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
|
||||||
|
const sorted = timetable.sort((a, b) => {
|
||||||
|
// Zuerst nach Datum
|
||||||
|
if (a.date !== b.date) {
|
||||||
|
return a.date - b.date;
|
||||||
|
}
|
||||||
|
// Falls Datum gleich, nach Startzeit
|
||||||
|
if (a.startTime !== b.startTime) {
|
||||||
|
return a.startTime - b.startTime;
|
||||||
|
}
|
||||||
|
// Falls Startzeit gleich, nach Endzeit
|
||||||
|
return a.endTime - b.endTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
let final = []
|
||||||
|
|
||||||
|
for (let i = 0; i < sorted.length - 1; i++) {
|
||||||
|
let now = sorted[i];
|
||||||
|
let next = sorted[i + 1];
|
||||||
|
//gleicher tag
|
||||||
|
if (now.date == next.date) {
|
||||||
|
if (now.endTime == next.startTime) {
|
||||||
|
now.endTime = next.endTime;
|
||||||
|
final.push(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Local IPs:', results);
|
pb.autoCancellation(false);
|
||||||
|
|
||||||
|
const records = await pb.collection('classes').getFullList();
|
||||||
|
//console.log(records);
|
||||||
|
|
||||||
|
for (let i = 0; i < final.length; i++) {
|
||||||
|
let existingRecords;
|
||||||
|
|
||||||
|
try {
|
||||||
|
existingRecords = await pb.collection('classes').getList(1, 0, {
|
||||||
|
filter: 'untis_id = "' + final[i].id + '"',
|
||||||
|
});
|
||||||
|
existingRecords = existingRecords.items[0];
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
//console.log(typeof existingRecords);
|
||||||
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
if (!existingRecords) {
|
||||||
console.log(`Example app listening on port ${port}`)
|
// Neu erstellen
|
||||||
})
|
const data = {
|
||||||
|
"untis_id": final[i].id,
|
||||||
|
"date": final[i].date,
|
||||||
|
"startTime": final[i].startTime,
|
||||||
|
"endTime": final[i].endTime,
|
||||||
|
"ro": JSON.stringify(final[i].ro),
|
||||||
|
"su": JSON.stringify(final[i].su),
|
||||||
|
"kl": JSON.stringify(final[i].kl),
|
||||||
|
"raw": JSON.stringify(final[i]),
|
||||||
|
"old_version": "JSON"
|
||||||
|
};
|
||||||
|
|
||||||
|
const record = await pb.collection('classes').create(data);
|
||||||
|
created++;
|
||||||
|
continue; // Überspringe die Erstellung, wenn der Datensatz bereits existiert
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log("Record with untis_id " + final[i].id + " already exists. Skipping creation.");
|
||||||
|
|
||||||
|
// Prüfe, ob etwas geändert wurde
|
||||||
|
const isChanged =
|
||||||
|
existingRecords.date != final[i].date ||
|
||||||
|
existingRecords.startTime != final[i].startTime ||
|
||||||
|
existingRecords.endTime != final[i].endTime ||
|
||||||
|
(existingRecords.kl[0].id != final[i].kl[0].id) ||
|
||||||
|
(existingRecords.ro[0].id != final[i].ro[0].id) ||
|
||||||
|
(existingRecords.su[0].id != final[i].su[0].id);
|
||||||
|
|
||||||
|
if (!isChanged) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiere den bestehenden Datensatz
|
||||||
|
existingRecords.old_version = existingRecords.raw;
|
||||||
|
existingRecords.raw = JSON.stringify(final[i]);
|
||||||
|
existingRecords.date = final[i].date;
|
||||||
|
existingRecords.startTime = final[i].startTime;
|
||||||
|
existingRecords.endTime = final[i].endTime;
|
||||||
|
existingRecords.kl = final[i].kl;
|
||||||
|
existingRecords.ro = final[i].ro;
|
||||||
|
existingRecords.su = final[i].su;
|
||||||
|
existingRecords.last_update = new Date().toISOString();
|
||||||
|
|
||||||
|
const record = await pb.collection('classes').update(existingRecords.id, existingRecords);
|
||||||
|
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Sync done — Created: ${created}, Updated: ${updated}, Skipped: ${skipped}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
await main();
|
||||||
|
}, 1000 * 60 * 30); // Alle 30 Minuten aktualisieren
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
"pocketbase": "^0.26.8",
|
||||||
"webuntis": "^2.2.1"
|
"webuntis": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -796,6 +797,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pocketbase": {
|
||||||
|
"version": "0.26.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.8.tgz",
|
||||||
|
"integrity": "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
|
"pocketbase": "^0.26.8",
|
||||||
"webuntis": "^2.2.1"
|
"webuntis": "^2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
readme.md
62
readme.md
@@ -1,33 +1,61 @@
|
|||||||
|
**Project**
|
||||||
|
- **Name:**: Simple WebUntis → PocketBase sync
|
||||||
|
- **Description:**: A small Node.js script that fetches your WebUntis timetable and synchronizes classes into a PocketBase collection.
|
||||||
|
|
||||||
# Moodle Timetable Server
|
**Files**
|
||||||
|
- **Main script:**: [index.js](index.js)
|
||||||
|
- **Package metadata:**: [package.json](package.json)
|
||||||
|
- **Dockerfile:**: [dockerfile](dockerfile)
|
||||||
|
|
||||||
A Node.js server that fetches timetable data from WebUntis.
|
**Requirements**
|
||||||
|
- **Node.js:**: v14+ recommended
|
||||||
|
- **Environment:**: A PocketBase instance reachable from the script (the URL is currently set inside `index.js`).
|
||||||
|
|
||||||
## Setup
|
**Dependencies**
|
||||||
|
- **webuntis**: fetches timetable data
|
||||||
|
- **pocketbase**: PocketBase client
|
||||||
|
- **luxon**: date handling
|
||||||
|
- **dotenv**: load environment variables
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
- **Environment variables:**: Create a `.env` in the project root with at least:
|
||||||
|
|
||||||
1. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install express dotenv luxon webuntis
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a `.env` file with:
|
|
||||||
```env
|
|
||||||
PORT=3000
|
|
||||||
WEBUNTIS_SCHOOL=your_school
|
WEBUNTIS_SCHOOL=your_school
|
||||||
WEBUNTIS_USER=your_username
|
WEBUNTIS_USER=your_username
|
||||||
WEBUNTIS_PASS=your_password
|
WEBUNTIS_PASS=your_password
|
||||||
WEBUNTIS_URL=your_webuntis_url
|
WEBUNTIS_URL=your_webuntis_url
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints
|
- **PocketBase URL:**: The script currently instantiates PocketBase with a hard-coded URL (`https://fsae41.de`). Edit `index.js` to change this or make it configurable via an env var.
|
||||||
|
|
||||||
- `GET /` - Health check
|
**Install**
|
||||||
- `GET /timetable` - Fetch timetable for next 4 weeks
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node server.js
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Server runs on `http://localhost:3000` by default.
|
**Run**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
The script runs an initial sync and then repeats every 30 minutes (see `setInterval` near the end of `index.js`).
|
||||||
|
|
||||||
|
**Behavior**
|
||||||
|
- **Merging lessons:**: The script sorts and merges adjacent lessons that end and start at the same time on the same day.
|
||||||
|
- **PocketBase collection:**: Records are created/updated in the `classes` collection. Existing records are compared and updated if changes are detected.
|
||||||
|
|
||||||
|
**Docker**
|
||||||
|
- A `dockerfile` is present; you can containerize the app. Ensure environment variables and PocketBase URL are provided to the container.
|
||||||
|
|
||||||
|
**Notes & Next steps**
|
||||||
|
- Consider moving the PocketBase URL into `.env` for easier configuration.
|
||||||
|
- Add error handling and logging for production use.
|
||||||
|
|
||||||
|
**License**
|
||||||
|
- No license specified. Add one if you plan to publish.
|
||||||
|
|
||||||
|
**Contact**
|
||||||
|
- Questions or improvements: edit `index.js` and open an issue or PR in your repo.
|
||||||
|
|||||||
Reference in New Issue
Block a user