1
0
mirror of https://github.com/XFox111/bonch-calendar.git synced 2026-04-22 07:08:01 +03:00

init: initial commit

This commit is contained in:
2025-11-18 20:16:48 +00:00
commit fe11e264de
69 changed files with 10008 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using BonchCalendar.Utils;
namespace BonchCalendar.Services;
public class ApiService
{
public async Task<Dictionary<int, string>> GetFacultiesListAsync() =>
ParseListResponse(await SendRequestAsync(new()
{
["choice"] = "1", // "choice" is always "1" (idk why, don't ask me)
["schet"] = GetCurrentSemesterId()
}));
public async Task<Dictionary<int, string>> GetGroupsListAsync(int facultyId, int course) =>
ParseListResponse(await SendRequestAsync(new()
{
["choice"] = "1",
["schet"] = GetCurrentSemesterId(),
["faculty"] = facultyId.ToString(), // Specifying faculty ID returns a list of groups
["kurs"] = course.ToString() // Course number is actually optional, but filters out other groups. Can be set 0 to get all groups of the faculty.
}));
public async Task<string> GetScheduleDocumentAsync(int groupId, TimetableType timetableType) =>
await SendRequestAsync(new()
{
["schet"] = GetCurrentSemesterId(),
["type_z"] = ((int)timetableType).ToString(),
["group"] = groupId.ToString()
});
public async Task<DateTime> GetSemesterStartDateAsync(int groupId)
{
using HttpClient client = new();
string content = await client.GetStringAsync($"https://www.sut.ru/studentu/raspisanie/raspisanie-zanyatiy-studentov-ochnoy-i-vecherney-form-obucheniya?group={groupId}");
using IHtmlDocument doc = new HtmlParser().ParseDocument(content);
string labelText = doc.QuerySelector("a#rasp-prev + div:nth-child(2) > span")!.TextContent;
int weekNumber = int.Parse(ParserUtils.NumberRegex().Match(labelText).Value);
DateTime currentDate = DateTime.Today;
currentDate = currentDate
.AddDays(-(int)currentDate.DayOfWeek + 1) // Move to Monday
.AddDays(-7 * (weekNumber - 1)); // Move back to the first week
return currentDate;
}
private static Dictionary<int, string> ParseListResponse(string responseContent) =>
responseContent
.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Split(','))
.ToDictionary(
parts => int.Parse(parts[0]),
parts => parts[1]
);
public async Task<string> SendRequestAsync(Dictionary<string, string> formData)
{
HttpRequestMessage request = new(HttpMethod.Post, "https://cabinet.sut.ru/raspisanie_all_new.php")
{
Content = new FormUrlEncodedContent(formData)
};
using HttpClient client = new(new HttpClientHandler
{
// Sometimes Bonch being Bonch just doesn't renew its SSL certificates properly
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
});
HttpResponseMessage response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
private static string GetCurrentSemesterId()
{
DateTime now = DateTime.Today;
int currentSemester = now.Month is >= 8 or < 2
? 1 // August through January - first semester
: 2; // Everything else - second
int termStartYear = now.Year - 2000; // We need only last two digits (e.g. 25 for 2025)
if (now.Month < 8) // Before August means we are in the second semester of the previous academic year
termStartYear--;
return $"205.{termStartYear}{termStartYear + 1}/{currentSemester}";
}
}
+138
View File
@@ -0,0 +1,138 @@
using System.Globalization;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using BonchCalendar.Utils;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
namespace BonchCalendar.Services;
public partial class ParsingService
{
public CalendarEvent[] ParseGeneralTimetable(string rawHtml, DateTime semesterStartDate, string groupName)
{
using IHtmlDocument doc = new HtmlParser().ParseDocument(rawHtml);
IHtmlCollection<IElement> rawClasses = doc.QuerySelectorAll(".pair");
List<CalendarEvent> classes = [];
foreach (IElement classItem in rawClasses)
{
var (className, classType, professors, auditorium) = ParseBaseInfo(classItem);
int weekday = int.Parse(classItem.GetAttribute("weekday")!);
string timeLabelText = classItem.ParentElement!.ParentElement!.Children[0].TextContent;
Match timeMatch = ParserUtils.TimeLabelRegex().Match(timeLabelText);
string number = timeMatch.Success ? timeMatch.Groups["number"].Value : timeLabelText;
(TimeSpan startTime, TimeSpan endTime) = !timeMatch.Success ?
ParserUtils.GetTimesFromLabel(timeLabelText) :
(
TimeSpan.Parse(timeMatch.Groups["start"].Value),
TimeSpan.Parse(timeMatch.Groups["end"].Value)
);
int[] weeks = [
.. ParserUtils.NumberRegex().Matches(classItem.QuerySelector(".weeks")!.TextContent)
.Select(i => int.Parse(i.Value))
];
foreach (int week in weeks)
{
DateTime classDate = semesterStartDate
.AddDays((week - 1) * 7) // Move to the correct week
.AddDays(weekday - 1); // Move to the correct weekday
classes.Add(GetEvent(
$"{number}. {className} ({classType})", auditorium,
GetDescription(groupName, professors, auditorium, weeks),
classDate, startTime, endTime));
}
}
return [.. classes];
}
public CalendarEvent[] ParseExamTimetable(string rawHtml, string groupName)
{
using IHtmlDocument doc = new HtmlParser().ParseDocument(rawHtml);
IHtmlCollection<IElement> rawClasses = doc.QuerySelectorAll(".pair");
List<CalendarEvent> classes = new(rawClasses.Count);
foreach (IElement classItem in rawClasses)
{
var (className, classType, professors, auditorium) = ParseBaseInfo(classItem);
DateTime classDate = DateTime.Parse(classItem.Children[0].ChildNodes[0].TextContent, CultureInfo.GetCultureInfo("ru-RU"));
Match timeMatch = ParserUtils.ExamTimeRegex().Match(classItem.GetAttribute("pair")!);
if (!timeMatch.Success)
timeMatch = ParserUtils.ExamTimeAltRegex().Match(classItem.GetAttribute("pair")!);
string number = timeMatch.Groups["number"].Success ?
$"{timeMatch.Groups["number"].Value}. " : string.Empty;
TimeSpan startTime = TimeSpan.Parse(timeMatch.Groups["start"].Value.Replace('.', ':'));
TimeSpan endTime = TimeSpan.Parse(timeMatch.Groups["end"].Value.Replace('.', ':'));
classes.Add(GetEvent(
$"{number}{className} ({classType})", auditorium,
GetDescription(groupName, professors, auditorium),
classDate, startTime, endTime));
}
return [.. classes];
}
private static CalendarEvent GetEvent(string title, string auditorium, string description, DateTime date, TimeSpan startTime, TimeSpan endTime) =>
new()
{
Summary = title,
Description = description,
Start = new CalDateTime(date.Add(startTime - TimeSpan.FromHours(3)).ToUniversalTime()),
End = new CalDateTime(date.Add(endTime - TimeSpan.FromHours(3)).ToUniversalTime()),
Location = auditorium
};
private static string GetDescription(string groupName, string[] professors, string auditorium, int[]? weeks = null)
{
string str = $"""
Группа: {groupName}
Преподаватель(и):
- {string.Join("\n- ", professors)}
""";
if (weeks is not null && weeks.Length > 0)
str += $"\nНедели: {string.Join(", ", weeks)}";
Match auditoriumMatch = ParserUtils.AuditoriumRegex().Match(auditorium);
if (!auditoriumMatch.Success)
auditoriumMatch = ParserUtils.AuditoriumAltRegex().Match(auditorium);
if (auditoriumMatch.Success)
str += "\n\n" + $"""
ГУТ.Навигатор:
https://nav.sut.ru/?cab=k{auditoriumMatch.Groups["wing"].Value}-{auditoriumMatch.Groups["room"].Value}
""";
return str;
}
private static (string className, string classType, string[] professors, string auditorium) ParseBaseInfo(IElement classElement)
{
string className = classElement.QuerySelector(".subect")!.TextContent;
string classType = classElement.QuerySelector(".type")!.TextContent
.Replace("(", string.Empty).Replace(")", string.Empty).Trim();
string[] professors = classElement.QuerySelector(".teacher[title]")!.GetAttribute("title")
!.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
string auditorium = classElement.QuerySelector(".aud")!.TextContent
.Replace("ауд.:", string.Empty).Replace(';', ',').Trim();
return (className, classType, professors, auditorium);
}
}