DB Trips iCal Saver

Adds "Add to Calendar" option for DB trips

  1. // ==UserScript==
  2. // @name DB Trips iCal Saver
  3. // @namespace https://github.com/tcpekin/deutsche-bahn-ics
  4. // @version 2024-03-26
  5. // @license MIT
  6. // @description Adds "Add to Calendar" option for DB trips
  7. // @author You
  8. // @match https://www.bahn.de/buchung/fahrplan/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=bahn.de
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15. /**
  16.  
  17. Here I use a slightly modified icsFormatter by @matthiasanderer
  18.  
  19. Credits: matthiasanderer (https://github.com/matthiasanderer/icsFormatter)
  20.  
  21. **/
  22. window.icsFormatter = function () {
  23. 'use strict';
  24.  
  25. if (navigator.userAgent.indexOf('MSIE') > -1 && navigator.userAgent.indexOf('MSIE 10') == -1) {
  26. console.log('Unsupported Browser');
  27. return;
  28. }
  29.  
  30. var SEPARATOR = (navigator.appVersion.indexOf('Win') !== -1) ? '\r\n' : '\n';
  31. var calendarEvents = [];
  32. var calendarStart = [
  33. 'BEGIN:VCALENDAR',
  34. 'VERSION:2.0'
  35. ].join(SEPARATOR);
  36. var calendarEnd = SEPARATOR + 'END:VCALENDAR';
  37.  
  38. return {
  39. 'events': function () {
  40. return calendarEvents;
  41. },
  42.  
  43. 'calendar': function () {
  44. return calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
  45. },
  46. 'addEvent': function (subject, description, location, begin, stop) {
  47. if (typeof subject === 'undefined' ||
  48. typeof description === 'undefined' ||
  49. typeof location === 'undefined' ||
  50. typeof begin === 'undefined' ||
  51. typeof stop === 'undefined'
  52. ) {
  53. return false;
  54. }
  55. var start_date = new Date(begin);
  56. var end_date = new Date(stop);
  57.  
  58. var start_year = ("0000" + (start_date.getFullYear().toString())).slice(-4);
  59. var start_month = ("00" + ((start_date.getMonth() + 1).toString())).slice(-2);
  60. var start_day = ("00" + ((start_date.getDate()).toString())).slice(-2);
  61. var start_hours = ("00" + (start_date.getHours().toString())).slice(-2);
  62. var start_minutes = ("00" + (start_date.getMinutes().toString())).slice(-2);
  63. var start_seconds = ("00" + (start_date.getMinutes().toString())).slice(-2);
  64.  
  65. var end_year = ("0000" + (end_date.getFullYear().toString())).slice(-4);
  66. var end_month = ("00" + ((end_date.getMonth() + 1).toString())).slice(-2);
  67. var end_day = ("00" + ((end_date.getDate()).toString())).slice(-2);
  68. var end_hours = ("00" + (end_date.getHours().toString())).slice(-2);
  69. var end_minutes = ("00" + (end_date.getMinutes().toString())).slice(-2);
  70. var end_seconds = ("00" + (end_date.getMinutes().toString())).slice(-2);
  71.  
  72. var start_time = '';
  73. var end_time = '';
  74. if (start_minutes + start_seconds + end_minutes + end_seconds !== 0) {
  75. start_time = 'T' + start_hours + start_minutes + start_seconds;
  76. end_time = 'T' + end_hours + end_minutes + end_seconds;
  77. }
  78.  
  79. var start = start_year + start_month + start_day + start_time;
  80. var end = end_year + end_month + end_day + end_time;
  81.  
  82. var calendarEvent = [
  83. 'BEGIN:VEVENT',
  84. 'CLASS:PUBLIC',
  85. 'DESCRIPTION:' + description,
  86. 'DTSTART:' + start,
  87. 'DTEND:' + end,
  88. 'LOCATION:' + location,
  89. 'SUMMARY;LANGUAGE=en-us:' + subject,
  90. 'TRANSP:TRANSPARENT',
  91. 'END:VEVENT'
  92. ].join(SEPARATOR);
  93.  
  94. calendarEvents.push(calendarEvent);
  95. return calendarEvent;
  96. },
  97.  
  98. 'download': function (filename, ext) {
  99. if (calendarEvents.length < 1) {
  100. return false;
  101. }
  102. var calendar = calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
  103. var a = document.createElement('a');
  104. a.href = "data:text/calendar;charset=utf8," + escape(calendar);
  105. a.download = 'db_trip.ics';
  106. document.getElementsByTagName('body')[0].appendChild(a);
  107. a.click();
  108. }
  109. };
  110. };
  111.  
  112. function parent(el, n) {
  113. while (n > 0) {
  114. el = el.parentNode;
  115. n--;
  116. }
  117. return el;
  118. };
  119.  
  120. function formatDate(inputDate) {
  121. // Split the input date string by space
  122. const parts = inputDate.split(' ');
  123.  
  124. // Map month names to their numeric representation (English)
  125. const monthMap = {
  126. 'Jan.': '01',
  127. 'Feb.': '02',
  128. 'März': '03',
  129. 'Apr.': '04',
  130. 'Mai': '05',
  131. 'Juni': '06',
  132. 'Juli': '07',
  133. 'Aug.': '08',
  134. 'Sep.': '09',
  135. 'Okt.': '10',
  136. 'Nov.': '11',
  137. 'Dez.': '12'
  138. };
  139.  
  140. // Extract day, month, and year
  141. const day = parts[1].slice(0, -1); // Remove the trailing dot
  142. const month = monthMap[parts[2]]; // Convert month to numeric format
  143. const year = parts[3];
  144. // console.log({day, month, year})
  145. // Pad day with leading zero if needed
  146. const paddedDay = day.length === 1 ? '0' + day : day;
  147.  
  148. // Return formatted date string
  149. return `${month}.${paddedDay}.${year}`;
  150. }
  151.  
  152.  
  153.  
  154. function main() {
  155. var actionMenuUl = document.querySelectorAll(".ActionMenu div div ul");
  156. actionMenuUl.forEach((element, i) => {
  157. if (element.querySelectorAll("li").length > 2) return;
  158. var addCalendarOption = document.createElement("li");
  159. addCalendarOption.className = "_content-button _content-button--with-icons add_to_calendar";
  160. addCalendarOption.setAttribute("style", "align-items: center; column-gap: .5rem; cursor: pointer; display: flex; padding: .75rem 1.0rem;");
  161. var spanEl = document.createElement("span");
  162. spanEl.className = "db-color--dbRed db-web-icon--custom-size icon-action-share db-web-icon";
  163. var spanElWithDesc = document.createElement("span");
  164. spanElWithDesc.innerHTML = "Add to calendar";
  165. addCalendarOption.appendChild(spanEl);
  166. addCalendarOption.appendChild(spanElWithDesc);
  167. addCalendarOption.addEventListener("click", function (e) { saveTripToICS(e.target) });
  168. element.appendChild(addCalendarOption);
  169.  
  170. parent(element, 2).setAttribute("style", "--item-count: 3;");
  171.  
  172. var style = document.createElement("style");
  173. style.innerHTML = '.add_to_calendar:hover { background: #f0f3f5; }';
  174. document.head.appendChild(style);
  175. });
  176. };
  177.  
  178. setInterval(main, 1000);
  179.  
  180. function waitFor(selectorFunc, applyFunc) {
  181. var itl = setInterval(function () {
  182. if (selectorFunc()) {
  183. clearInterval(itl);
  184. applyFunc();
  185. }
  186. }, 50);
  187. }
  188.  
  189.  
  190. function saveTripToICS(targetElement) {
  191. var trip = parent(targetElement, 7);
  192. trip.querySelector(".reiseplan__details").style.display = "none";
  193. trip.querySelector(".reiseplan__details button").click();
  194. waitFor(
  195. function () {
  196. return trip.querySelector(".reise-details__infos") != null && trip.querySelector("ri-transport-chip").getAttribute("transport-text") != null
  197. },
  198. function () {
  199. trip.querySelector(".reise-details__infos").style.display = "none";
  200. trip.querySelector(".reise-details__actions").style.display = "none";
  201. var tripParts = trip.querySelectorAll(".verbindungs-abschnitt");
  202. var parsedTripParts = parseTripParts(tripParts);
  203.  
  204. window.calEntry = window.icsFormatter();
  205. var nextDayFlag = 0;
  206. var lastEnd = new Date(1991, 3, 9);
  207. parsedTripParts.forEach((part, i) => {
  208. var stringDate = formatDate(document.querySelector(".default-reiseloesung-list-page-controls__title-date").innerText);
  209. var begin = new Date(stringDate + ", " + part.startTime);
  210. var end = new Date(stringDate + ", " + part.endTime);
  211. // Move forward a day if the beginning is before the last end. This occurs when you have a pause between trains that crosses days.
  212. if (begin < lastEnd) {
  213. nextDayFlag = 1; // Move the whole trip to the next day
  214. }
  215.  
  216. // Apply next day flag if set
  217. if (nextDayFlag === 1) {
  218. begin.setDate(begin.getDate() + 1); // Move begin date to the next day
  219. end.setDate(end.getDate() + 1); // Move end date to the next day
  220. }
  221. // Adjust dates if the end time is before the start time - this is when a train crosses midnight
  222. if (end < begin) {
  223. nextDayFlag = 1; // Move end date to the next day
  224. end.setDate(end.getDate() + 1); // Add a day to end date
  225. }
  226.  
  227. lastEnd = end;
  228. //console.log({begin, end})
  229. var title = part.eventName;
  230. window.calEntry.addEvent(title, part.eventDescription, "", begin.toUTCString(), end.toUTCString());
  231. });
  232.  
  233. window.calEntry.download("db_trip");
  234. trip.querySelector(".reiseplan__details button").click();
  235. trip.querySelector(".reiseplan__details").style.display = "";
  236. trip.querySelector(".reise-details__infos").style.display = "";
  237. trip.querySelector(".reise-details__actions").style.display = "";
  238. }
  239.  
  240. );
  241. }
  242.  
  243. function parseTripParts(tripParts) {
  244.  
  245. var result = [];
  246.  
  247. tripParts.forEach((part, i) => {
  248. var trainName = part.querySelector("ri-transport-chip").getAttribute("transport-text");
  249. var timeEls = part.querySelectorAll("time");
  250. var startTime = timeEls[0].innerText;
  251. var endTime = timeEls[timeEls.length - 1].innerText;
  252. var stopsEls = part.querySelectorAll(".verbindungs-halt");
  253. var fromStation = stopsEls[0].querySelector(".verbindungs-halt-bahnhofsinfos__name--abfahrt").innerText;
  254. var fromTrack = stopsEls[0].querySelector(".verbindungs-abschnitt-zeile__gleis").innerText;
  255. var toStation = stopsEls[1].querySelector(".verbindungs-halt-bahnhofsinfos__name--ankunft").innerText;
  256. var toTrack = stopsEls[1].querySelector(".verbindungs-abschnitt-zeile__gleis").innerText;
  257. result.push({
  258. startTime: startTime,
  259. endTime: endTime,
  260. eventName: `(${trainName}) ${fromStation} - ${toStation}`,
  261. eventDescription: `${trainName} ${fromStation} (${fromTrack}) - ${toStation} (${toTrack})`
  262. });
  263. });
  264.  
  265. return result;
  266. }
  267.  
  268.  
  269. })();