- // ==UserScript==
- // @name DB Trips iCal Saver
- // @namespace https://github.com/tcpekin/deutsche-bahn-ics
- // @version 2024-03-26
- // @license MIT
- // @description Adds "Add to Calendar" option for DB trips
- // @author You
- // @match https://www.bahn.de/buchung/fahrplan/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=bahn.de
- // @grant none
- // ==/UserScript==
-
- (function () {
- 'use strict';
- /**
-
- Here I use a slightly modified icsFormatter by @matthiasanderer
-
- Credits: matthiasanderer (https://github.com/matthiasanderer/icsFormatter)
-
- **/
- window.icsFormatter = function () {
- 'use strict';
-
- if (navigator.userAgent.indexOf('MSIE') > -1 && navigator.userAgent.indexOf('MSIE 10') == -1) {
- console.log('Unsupported Browser');
- return;
- }
-
- var SEPARATOR = (navigator.appVersion.indexOf('Win') !== -1) ? '\r\n' : '\n';
- var calendarEvents = [];
- var calendarStart = [
- 'BEGIN:VCALENDAR',
- 'VERSION:2.0'
- ].join(SEPARATOR);
- var calendarEnd = SEPARATOR + 'END:VCALENDAR';
-
- return {
- 'events': function () {
- return calendarEvents;
- },
-
- 'calendar': function () {
- return calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
- },
- 'addEvent': function (subject, description, location, begin, stop) {
- if (typeof subject === 'undefined' ||
- typeof description === 'undefined' ||
- typeof location === 'undefined' ||
- typeof begin === 'undefined' ||
- typeof stop === 'undefined'
- ) {
- return false;
- }
- var start_date = new Date(begin);
- var end_date = new Date(stop);
-
- var start_year = ("0000" + (start_date.getFullYear().toString())).slice(-4);
- var start_month = ("00" + ((start_date.getMonth() + 1).toString())).slice(-2);
- var start_day = ("00" + ((start_date.getDate()).toString())).slice(-2);
- var start_hours = ("00" + (start_date.getHours().toString())).slice(-2);
- var start_minutes = ("00" + (start_date.getMinutes().toString())).slice(-2);
- var start_seconds = ("00" + (start_date.getMinutes().toString())).slice(-2);
-
- var end_year = ("0000" + (end_date.getFullYear().toString())).slice(-4);
- var end_month = ("00" + ((end_date.getMonth() + 1).toString())).slice(-2);
- var end_day = ("00" + ((end_date.getDate()).toString())).slice(-2);
- var end_hours = ("00" + (end_date.getHours().toString())).slice(-2);
- var end_minutes = ("00" + (end_date.getMinutes().toString())).slice(-2);
- var end_seconds = ("00" + (end_date.getMinutes().toString())).slice(-2);
-
- var start_time = '';
- var end_time = '';
- if (start_minutes + start_seconds + end_minutes + end_seconds !== 0) {
- start_time = 'T' + start_hours + start_minutes + start_seconds;
- end_time = 'T' + end_hours + end_minutes + end_seconds;
- }
-
- var start = start_year + start_month + start_day + start_time;
- var end = end_year + end_month + end_day + end_time;
-
- var calendarEvent = [
- 'BEGIN:VEVENT',
- 'CLASS:PUBLIC',
- 'DESCRIPTION:' + description,
- 'DTSTART:' + start,
- 'DTEND:' + end,
- 'LOCATION:' + location,
- 'SUMMARY;LANGUAGE=en-us:' + subject,
- 'TRANSP:TRANSPARENT',
- 'END:VEVENT'
- ].join(SEPARATOR);
-
- calendarEvents.push(calendarEvent);
- return calendarEvent;
- },
-
- 'download': function (filename, ext) {
- if (calendarEvents.length < 1) {
- return false;
- }
- var calendar = calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
- var a = document.createElement('a');
- a.href = "data:text/calendar;charset=utf8," + escape(calendar);
- a.download = 'db_trip.ics';
- document.getElementsByTagName('body')[0].appendChild(a);
- a.click();
- }
- };
- };
-
- function parent(el, n) {
- while (n > 0) {
- el = el.parentNode;
- n--;
- }
- return el;
- };
-
- function formatDate(inputDate) {
- // Split the input date string by space
- const parts = inputDate.split(' ');
-
- // Map month names to their numeric representation (English)
- const monthMap = {
- 'Jan.': '01',
- 'Feb.': '02',
- 'März': '03',
- 'Apr.': '04',
- 'Mai': '05',
- 'Juni': '06',
- 'Juli': '07',
- 'Aug.': '08',
- 'Sep.': '09',
- 'Okt.': '10',
- 'Nov.': '11',
- 'Dez.': '12'
- };
-
- // Extract day, month, and year
- const day = parts[1].slice(0, -1); // Remove the trailing dot
- const month = monthMap[parts[2]]; // Convert month to numeric format
- const year = parts[3];
- // console.log({day, month, year})
- // Pad day with leading zero if needed
- const paddedDay = day.length === 1 ? '0' + day : day;
-
- // Return formatted date string
- return `${month}.${paddedDay}.${year}`;
- }
-
-
-
- function main() {
- var actionMenuUl = document.querySelectorAll(".ActionMenu div div ul");
- actionMenuUl.forEach((element, i) => {
- if (element.querySelectorAll("li").length > 2) return;
- var addCalendarOption = document.createElement("li");
- addCalendarOption.className = "_content-button _content-button--with-icons add_to_calendar";
- addCalendarOption.setAttribute("style", "align-items: center; column-gap: .5rem; cursor: pointer; display: flex; padding: .75rem 1.0rem;");
- var spanEl = document.createElement("span");
- spanEl.className = "db-color--dbRed db-web-icon--custom-size icon-action-share db-web-icon";
- var spanElWithDesc = document.createElement("span");
- spanElWithDesc.innerHTML = "Add to calendar";
- addCalendarOption.appendChild(spanEl);
- addCalendarOption.appendChild(spanElWithDesc);
- addCalendarOption.addEventListener("click", function (e) { saveTripToICS(e.target) });
- element.appendChild(addCalendarOption);
-
- parent(element, 2).setAttribute("style", "--item-count: 3;");
-
- var style = document.createElement("style");
- style.innerHTML = '.add_to_calendar:hover { background: #f0f3f5; }';
- document.head.appendChild(style);
- });
- };
-
- setInterval(main, 1000);
-
- function waitFor(selectorFunc, applyFunc) {
- var itl = setInterval(function () {
- if (selectorFunc()) {
- clearInterval(itl);
- applyFunc();
- }
- }, 50);
- }
-
-
- function saveTripToICS(targetElement) {
- var trip = parent(targetElement, 7);
- trip.querySelector(".reiseplan__details").style.display = "none";
- trip.querySelector(".reiseplan__details button").click();
- waitFor(
- function () {
- return trip.querySelector(".reise-details__infos") != null && trip.querySelector("ri-transport-chip").getAttribute("transport-text") != null
- },
- function () {
- trip.querySelector(".reise-details__infos").style.display = "none";
- trip.querySelector(".reise-details__actions").style.display = "none";
- var tripParts = trip.querySelectorAll(".verbindungs-abschnitt");
- var parsedTripParts = parseTripParts(tripParts);
-
- window.calEntry = window.icsFormatter();
- var nextDayFlag = 0;
- var lastEnd = new Date(1991, 3, 9);
- parsedTripParts.forEach((part, i) => {
- var stringDate = formatDate(document.querySelector(".default-reiseloesung-list-page-controls__title-date").innerText);
- var begin = new Date(stringDate + ", " + part.startTime);
- var end = new Date(stringDate + ", " + part.endTime);
- // Move forward a day if the beginning is before the last end. This occurs when you have a pause between trains that crosses days.
- if (begin < lastEnd) {
- nextDayFlag = 1; // Move the whole trip to the next day
- }
-
- // Apply next day flag if set
- if (nextDayFlag === 1) {
- begin.setDate(begin.getDate() + 1); // Move begin date to the next day
- end.setDate(end.getDate() + 1); // Move end date to the next day
- }
- // Adjust dates if the end time is before the start time - this is when a train crosses midnight
- if (end < begin) {
- nextDayFlag = 1; // Move end date to the next day
- end.setDate(end.getDate() + 1); // Add a day to end date
- }
-
- lastEnd = end;
- //console.log({begin, end})
- var title = part.eventName;
- window.calEntry.addEvent(title, part.eventDescription, "", begin.toUTCString(), end.toUTCString());
- });
-
- window.calEntry.download("db_trip");
- trip.querySelector(".reiseplan__details button").click();
- trip.querySelector(".reiseplan__details").style.display = "";
- trip.querySelector(".reise-details__infos").style.display = "";
- trip.querySelector(".reise-details__actions").style.display = "";
- }
-
- );
- }
-
- function parseTripParts(tripParts) {
-
- var result = [];
-
- tripParts.forEach((part, i) => {
- var trainName = part.querySelector("ri-transport-chip").getAttribute("transport-text");
- var timeEls = part.querySelectorAll("time");
- var startTime = timeEls[0].innerText;
- var endTime = timeEls[timeEls.length - 1].innerText;
- var stopsEls = part.querySelectorAll(".verbindungs-halt");
- var fromStation = stopsEls[0].querySelector(".verbindungs-halt-bahnhofsinfos__name--abfahrt").innerText;
- var fromTrack = stopsEls[0].querySelector(".verbindungs-abschnitt-zeile__gleis").innerText;
- var toStation = stopsEls[1].querySelector(".verbindungs-halt-bahnhofsinfos__name--ankunft").innerText;
- var toTrack = stopsEls[1].querySelector(".verbindungs-abschnitt-zeile__gleis").innerText;
- result.push({
- startTime: startTime,
- endTime: endTime,
- eventName: `(${trainName}) ${fromStation} - ${toStation}`,
- eventDescription: `${trainName} ${fromStation} (${fromTrack}) - ${toStation} (${toTrack})`
- });
- });
-
- return result;
- }
-
-
- })();