1212from typing import List , Dict , Optional
1313import os
1414import pickle
15+ import holidays
16+ from pathlib import Path
1517from google .auth .transport .requests import Request
1618from google .oauth2 .credentials import Credentials
1719from google_auth_oauthlib .flow import InstalledAppFlow
@@ -79,12 +81,28 @@ def to_dict(self):
7981class ImprovedUntisParser :
8082 """Verbesserter Parser der mehrere Tage/Wochen unterstützt"""
8183
82- def __init__ (self , json_file : str ):
84+ def __init__ (self , json_file : str , bundesland : str = 'BY' ):
8385 with open (json_file , 'r' , encoding = 'utf-8' ) as f :
8486 self .data = json .load (f )
8587
8688 self .base_date = self ._extract_date_from_url ()
8789
90+ # Initialisiere deutsche Feiertage für das entsprechende Bundesland
91+ year = int (self .base_date [:4 ])
92+ self .holidays = holidays .Germany (years = year , prov = bundesland )
93+
94+ # Lade manuelle schulfreie Tage
95+ custom_holidays_file = Path (__file__ ).parent / 'school_holidays.json'
96+ if custom_holidays_file .exists ():
97+ with open (custom_holidays_file , 'r' , encoding = 'utf-8' ) as f :
98+ custom_data = json .load (f )
99+ for date_str in custom_data .get ('custom_holidays' , []):
100+ date_obj = datetime .strptime (date_str , '%Y-%m-%d' ).date ()
101+ self .holidays [date_obj ] = 'Schulfrei (manuell konfiguriert)'
102+ print (f" → { len (custom_data .get ('custom_holidays' , []))} manuelle schulfreie Tage geladen" )
103+
104+ print (f" → Feiertage geladen für { bundesland } { year } + manuelle Einträge" )
105+
88106 def _extract_date_from_url (self ) -> str :
89107 """Extrahiert das Datum aus der URL"""
90108 url = self .data ['timetable' ]['url' ]
@@ -102,61 +120,135 @@ def parse_lessons(self) -> List[UntisLesson]:
102120 print (f"Basis-Datum aus URL: { self .base_date } " )
103121
104122 # Versuche die Wochenstruktur zu erkennen
105- current_day_offset = 0 # Montag = 0, Dienstag = 1, etc.
106- last_start_time = None
107- last_lesson_card_index = - 1
123+ # Montag des base_date ermitteln (Wochenanfang)
124+ base = datetime .strptime (self .base_date , '%Y-%m-%d' )
125+ # Finde den Montag dieser Woche
126+ days_since_monday = base .weekday () # 0=Monday, 6=Sunday
127+ week_start = base - timedelta (days = days_since_monday )
108128
109- i = 0
110- while i < len (raw_lessons ):
111- lesson = raw_lessons [i ]
129+ # NEUE STRATEGIE: Gruppiere Lessons nach Tagen
130+ # Sammle erst alle Lessons mit ihrer Zeit
131+ lesson_groups = [] # [(index, start_time), ...]
132+
133+ for i , lesson in enumerate (raw_lessons ):
112134 class_name = lesson .get ('className' , '' )
113135 text = lesson .get ('text' , '' ).strip ()
114136
115- # Prüfe ob das ein Lesson-Card Container ist
116- # Nur die Haupt-lesson-card zählt (nicht lesson-card-container, etc.)
117137 if 'lesson-card ' in class_name or class_name .startswith ('lesson-card ' ):
118- # Extrahiere Zeit aus diesem Lesson
119138 time_match = re .search (r'(\d{2}):(\d{2})' , text )
120139 if time_match :
121- current_time = f"{ time_match .group (1 )} :{ time_match .group (2 )} "
122-
123- # Wenn die Zeit KLEINER ist als die letzte Zeit UND
124- # wir haben schon mindestens eine Lesson gesehen,
125- # dann ist das ein neuer Tag
126- if last_start_time and current_time < last_start_time :
127- current_day_offset += 1
128- print (f" → Tag wechsel erkannt bei Index { i } : { last_start_time } → { current_time } , jetzt Tag { current_day_offset } " )
129-
130- last_start_time = current_time
131-
132- # Parse diese Lesson
133- parsed = self ._parse_lesson_card (raw_lessons , i , current_day_offset )
134- if parsed :
135- lessons .append (parsed )
136- print (f" ✓ Tag { current_day_offset } → { parsed .date } ({ datetime .strptime (parsed .date , '%Y-%m-%d' ).strftime ('%A' )} ): { parsed .start_time } -{ parsed .end_time } { parsed .subject } @ { parsed .room } " )
137-
138- last_lesson_card_index = i
140+ start_time = f"{ time_match .group (1 )} :{ time_match .group (2 )} "
141+ lesson_groups .append ((i , start_time ))
142+
143+ # Erkenne Tagwechsel durch Zeit-Rücksprünge
144+ day_boundaries = [0 ] # Erste Lesson ist immer Start von Tag 0
145+ for idx , (i , time ) in enumerate (lesson_groups [1 :], start = 1 ):
146+ prev_time = lesson_groups [idx - 1 ][1 ]
147+ if time < prev_time : # Zeit springt zurück = neuer Tag
148+ day_boundaries .append (idx )
149+
150+ print (f" → Erkannte Tagesgrenzen bei Lesson-Indices: { day_boundaries } " )
151+ print (f" → Das entspricht { len (day_boundaries )} Tagen in den Daten" )
152+
153+ # Zähle Lessons pro Tag-Block um fehlende Tage zu erkennen
154+ day_lesson_counts = []
155+ for day_idx , boundary_start in enumerate (day_boundaries ):
156+ if day_idx < len (day_boundaries ) - 1 :
157+ boundary_end = day_boundaries [day_idx + 1 ]
158+ else :
159+ boundary_end = len (lesson_groups )
160+ lesson_count = boundary_end - boundary_start
161+ day_lesson_counts .append (lesson_count )
162+
163+ print (f" → Lessons pro Tag: { day_lesson_counts } " )
164+
165+ # INTELLIGENTE FEIERTAGS-ERKENNUNG
166+ # Prüfe welche Wochentage in dieser Woche Feiertage sind
167+ num_days_detected = len (day_boundaries )
168+
169+ # Erstelle Liste aller Wochentage (Mo-Fr = 0-4)
170+ all_weekdays = list (range (5 )) # [0, 1, 2, 3, 4]
171+
172+ # Prüfe welche Tage Feiertage sind
173+ holiday_weekdays = []
174+ for weekday in range (5 ): # Mo-Fr
175+ date = week_start + timedelta (days = weekday )
176+ if date in self .holidays :
177+ holiday_name = self .holidays .get (date )
178+ print (f" 🎊 Feiertag erkannt: { date .strftime ('%Y-%m-%d' )} ({ ['Mo' ,'Di' ,'Mi' ,'Do' ,'Fr' ][weekday ]} ) = { holiday_name } " )
179+ holiday_weekdays .append (weekday )
180+
181+ # Berechne welche Tage Schultage sind (ohne Feiertage)
182+ school_days = [d for d in all_weekdays if d not in holiday_weekdays ]
183+ expected_days = len (school_days )
184+
185+ print (f" → Erwartete Schultage: { expected_days } (ohne { len (holiday_weekdays )} Feiertage)" )
186+
187+ # Mapping: Welcher Tag-Index gehört zu welchem Wochentag?
188+ if num_days_detected == expected_days :
189+ # Perfekt: Anzahl passt
190+ weekday_mapping = school_days
191+ print (f" ✓ Mapping: { num_days_detected } Tage → { [['Mo' ,'Di' ,'Mi' ,'Do' ,'Fr' ][w ] for w in weekday_mapping ]} " )
192+ elif num_days_detected < expected_days :
193+ # Weniger Tage als erwartet - nimm die ersten N Schultage
194+ weekday_mapping = school_days [:num_days_detected ]
195+ print (f" ⚠ Nur { num_days_detected } Tage erkannt, erwartet { expected_days } " )
196+ print (f" → Mapping: { [['Mo' ,'Di' ,'Mi' ,'Do' ,'Fr' ][w ] for w in weekday_mapping ]} " )
197+ else :
198+ # Mehr Tage als erwartet - sollte nicht passieren, aber fallback
199+ print (f" ⚠ Mehr Tage ({ num_days_detected } ) als erwartet ({ expected_days } )!" )
200+ weekday_mapping = list (range (num_days_detected ))
201+
202+ # Weise jedem Lesson-Index den richtigen Wochentag zu
203+ lesson_to_weekday = {}
204+ for day_idx , boundary_start in enumerate (day_boundaries ):
205+ if day_idx < len (day_boundaries ) - 1 :
206+ boundary_end = day_boundaries [day_idx + 1 ]
207+ else :
208+ boundary_end = len (lesson_groups )
209+
210+ lesson_count = boundary_end - boundary_start
211+ actual_weekday = weekday_mapping [day_idx ]
212+ weekday_name = ['Mo' ,'Di' ,'Mi' ,'Do' ,'Fr' ,'Sa' ,'So' ][actual_weekday ]
213+
214+ # Weise alle Lessons in diesem Block zum richtigen Wochentag zu
215+ for lesson_idx in range (boundary_start , boundary_end ):
216+ actual_index = lesson_groups [lesson_idx ][0 ]
217+ lesson_to_weekday [actual_index ] = actual_weekday
218+
219+ print (f" → Tag-Block { day_idx } ({ lesson_count } Lessons) → Wochentag { actual_weekday } ({ weekday_name } )" )
220+
221+ # Parse alle Lessons mit dem richtigen Wochentag
222+ i = 0
223+ while i < len (raw_lessons ):
224+ lesson = raw_lessons [i ]
225+ class_name = lesson .get ('className' , '' )
226+
227+ if 'lesson-card ' in class_name or class_name .startswith ('lesson-card ' ):
228+ if i in lesson_to_weekday :
229+ weekday = lesson_to_weekday [i ]
230+ parsed = self ._parse_lesson_card (raw_lessons , i , week_start , weekday )
231+ if parsed :
232+ lessons .append (parsed )
233+ print (f" ✓ { parsed .date } ({ datetime .strptime (parsed .date , '%Y-%m-%d' ).strftime ('%A' )} ): { parsed .start_time } -{ parsed .end_time } { parsed .subject } @ { parsed .room } " )
139234
140235 i += 1
141236
142237 # Sortiere nach Datum und Zeit
143238 lessons .sort (key = lambda l : (l .date , l .start_time ))
144239
145- print (f"\n ✓ { len (lessons )} Lessons über { current_day_offset + 1 } Tage gefunden" )
240+ # Zähle eindeutige Tage
241+ unique_days = len (set (l .date for l in lessons ))
242+ print (f"\n ✓ { len (lessons )} Lessons über { unique_days } Tage gefunden" )
146243
147244 return lessons
148245
149- def _parse_lesson_card (self , lessons_list : List [Dict ], start_index : int , day_offset : int ) -> Optional [UntisLesson ]:
246+ def _parse_lesson_card (self , lessons_list : List [Dict ], start_index : int , week_start : datetime , weekday : int ) -> Optional [UntisLesson ]:
150247 """Parst eine einzelne Lesson-Card"""
151248 try :
152- # Berechne das Datum basierend auf day_offset
153- base = datetime .strptime (self .base_date , '%Y-%m-%d' )
154-
155- # Wenn base_date ein Montag ist, addiere day_offset Tage
156- # Ansonsten finde den Montag dieser Woche
157- weekday = base .weekday () # 0 = Montag
158- monday = base - timedelta (days = weekday )
159- lesson_date = monday + timedelta (days = day_offset )
249+ # Berechne das echte Datum basierend auf Wochentag
250+ # week_start ist Montag, weekday 0=Mo, 1=Di, 2=Mi, ...
251+ lesson_date = week_start + timedelta (days = weekday )
160252 lesson_date_str = lesson_date .strftime ('%Y-%m-%d' )
161253
162254 main_text = lessons_list [start_index ].get ('text' , '' ).strip ()
0 commit comments