using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.Serialization; namespace BonchCalendar.Services; /// /// Service for retrieving timetable. /// public class TimetableService( ApiService apiService, ParsingService parsingService, ILogger logger, IHostEnvironment environment ) { /// /// Try to retrieve timetable from application's cache. /// /// ID of a group to retrieve timetable for. /// null if cache for this timetable is not present, or is older than 6 hours. Otherwise, timetable content in iCal format. public async Task TryGetTimetableFromCacheAsync(int groupId) { string cacheFile = GetCachePath(groupId); if (!File.Exists(cacheFile) || (DateTime.UtcNow - File.GetLastWriteTimeUtc(cacheFile)).TotalHours >= 6) return null; if (environment.IsDevelopment()) { logger.LogWarning("Caching is disabled for development environment."); return null; } logger.LogInformation("Calendar for group {GroupId} is present in cache ({CacheFile}).", groupId, cacheFile); return await File.ReadAllTextAsync(cacheFile); } /// /// Retrieve timetable for specified group from sut.ru API. /// /// If set to true, result timetable will be wirtten to cache for that group. /// Action delegate that can be used to manipulate the result object, before converting it to iCal. /// A string that contains timetable in iCal format. public async Task GetTimetableAsync(int facultyId, int groupId, bool saveToCache = true, Action? transform = null) { // We need semester start date, since the regular timetable is represented on sut.ru semester week numbers. DateTime semesterStartDate = await apiService.GetSemesterStartDateAsync(groupId); string groupName = (await apiService.GetGroupsListAsync(facultyId, 0))[groupId]; // Retrieve and parse regular timetable first, since it's the only timetable that has different structure. string classesRaw = await apiService.GetScheduleDocumentAsync(groupId, TimetableType.Classes); List timetable = [.. parsingService.ParseGeneralTimetable(classesRaw, semesterStartDate, groupName)]; // Retrieve and parse other timetables using a loop, since they all can be parsed using the same parser. TimetableType[] types = [TimetableType.Attestations, TimetableType.Exams, TimetableType.ExamsForExtramural]; foreach (TimetableType type in types) { classesRaw = await apiService.GetScheduleDocumentAsync(groupId, type); timetable.AddRange(parsingService.ParseExamTimetable(classesRaw, groupName)); } // Create and configure the calendar. Calendar calendar = new(); calendar.AddTimeZone(new VTimeZone("Europe/Moscow")); calendar.Properties.Add(new CalendarProperty("X-WR-CALNAME", groupName)); calendar.Properties.Add(new CalendarProperty("X-WR-TIMEZONE", "Europe/Moscow")); calendar.Properties.Add(new CalendarProperty("REFRESH-INTERVAL;VALUE=DURATION", "PT6H")); // Specifies how often calendar client should poll for new timetable. calendar.Events.AddRange(timetable); transform?.Invoke(calendar); // If transform delegate is not null, invoke it. // Serialize calendar to iCal format. string content = new CalendarSerializer().SerializeToString(calendar)!; if (saveToCache) { string cacheFile = GetCachePath(groupId); await File.WriteAllTextAsync(cacheFile, content); logger.LogInformation("Cache updated: {CacheFile}", cacheFile); } return content; } private static string GetCachePath(int groupId) => Path.Combine(Path.GetTempPath(), $"bonch_cal_{groupId}.ics"); }