From abbccc935cd2be44c8944467a7509046dcb57b3d Mon Sep 17 00:00:00 2001 From: ddanilyuk Date: Sat, 2 Jul 2022 11:31:38 +0300 Subject: [PATCH] Add V2 Rozklad API --- .../App/Controllers/LessonsController.swift | 22 ---- ...ntroller.swift => RozkladController.swift} | 16 ++- .../App/Controllers/RozkladControllerV2.swift | 51 +++++++++ Sources/App/Cron/RefreshGroupsCron.swift | 2 +- .../GroupModelV2ClientResponse.swift | 14 +++ .../LessonsV2ClientResponse.swift | 83 ++++++++++++++ Sources/App/Models/RozkladV2/GroupV2.swift | 16 +++ .../Models/RozkladV2/GroupsResponseV2.swift | 15 +++ Sources/App/Models/RozkladV2/LessonV2.swift | 103 ++++++++++++++++++ .../Models/RozkladV2/LessonsResponseV2.swift | 21 ++++ Sources/App/configure.swift | 3 + Sources/App/routes.swift | 8 +- Sources/KPIHubParser/Models/Lesson.swift | 4 +- Sources/KPIHubParser/Models/RawLesson.swift | 26 ++++- 14 files changed, 352 insertions(+), 32 deletions(-) delete mode 100644 Sources/App/Controllers/LessonsController.swift rename Sources/App/Controllers/{GroupsController.swift => RozkladController.swift} (88%) create mode 100644 Sources/App/Controllers/RozkladControllerV2.swift create mode 100644 Sources/App/Models/RozkladV2/ClientResponse/GroupModelV2ClientResponse.swift create mode 100644 Sources/App/Models/RozkladV2/ClientResponse/LessonsV2ClientResponse.swift create mode 100644 Sources/App/Models/RozkladV2/GroupV2.swift create mode 100644 Sources/App/Models/RozkladV2/GroupsResponseV2.swift create mode 100644 Sources/App/Models/RozkladV2/LessonV2.swift create mode 100644 Sources/App/Models/RozkladV2/LessonsResponseV2.swift diff --git a/Sources/App/Controllers/LessonsController.swift b/Sources/App/Controllers/LessonsController.swift deleted file mode 100644 index 4443aea..0000000 --- a/Sources/App/Controllers/LessonsController.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// LessonsController.swift -// -// -// Created by Denys Danyliuk on 27.05.2022. -// - -import Vapor -import KPIHubParser - -final class LessonsController { - - func getLessons(for groupUUID: UUID, request: Request) async throws -> LessonsResponse { - let response = try await request.client.get( - "http://rozklad.kpi.ua/Schedules/ViewSchedule.aspx?g=\(groupUUID.uuidString)" - ) - let html = try (response.body).htmlString(encoding: .utf8) - let lessons = try LessonsParser().parse(html) - return LessonsResponse(id: groupUUID, lessons: lessons) - } - -} diff --git a/Sources/App/Controllers/GroupsController.swift b/Sources/App/Controllers/RozkladController.swift similarity index 88% rename from Sources/App/Controllers/GroupsController.swift rename to Sources/App/Controllers/RozkladController.swift index c8f6062..a8ca143 100644 --- a/Sources/App/Controllers/GroupsController.swift +++ b/Sources/App/Controllers/RozkladController.swift @@ -1,5 +1,5 @@ // -// GroupsController.swift +// RozkladController.swift // // // Created by Denys Danyliuk on 20.05.2022. @@ -12,7 +12,8 @@ import FluentPostgresDriver import Foundation import Routes -final class GroupsController { +/// This client is base on parsing groups and lessons from rozklad.kpi.ua +final class RozkladController { static let ukrainianAlphabet: [String] = [ "а", "б", "в", "г", "д", "е", "є", "ж", "з", "и", "і", @@ -59,7 +60,7 @@ final class GroupsController { var numberOfParsedGroups = 0 // Receiving groups names from first endpoint - let groupsNames = try await GroupsController.ukrainianAlphabet + let groupsNames = try await RozkladController.ukrainianAlphabet .asyncMap { letter -> AllGroupsClientResponse in let response: ClientResponse = try await client.post( "http://rozklad.kpi.ua/Schedules/ScheduleGroupSelection.aspx/GetGroups", @@ -101,6 +102,15 @@ final class GroupsController { .uniqued() } + func getLessons(for groupUUID: UUID, request: Request) async throws -> LessonsResponse { + let response = try await request.client.get( + "http://rozklad.kpi.ua/Schedules/ViewSchedule.aspx?g=\(groupUUID.uuidString)" + ) + let html = try (response.body).htmlString(encoding: .utf8) + let lessons = try LessonsParser().parse(html) + return LessonsResponse(id: groupUUID, lessons: lessons) + } + func scheduleGroupSelectionParameters(with groupName: String) -> String { "ctl00_ToolkitScriptManager_HiddenField=&__VIEWSTATE=%2FwEMDAwQAgAADgEMBQAMEAIAAA4BDAUDDBACAAAOAgwFBwwQAgwPAgEIQ3NzQ2xhc3MBD2J0biBidG4tcHJpbWFyeQEEXyFTQgUCAAAADAUNDBACAAAOAQwFAQwQAgAADgEMBQ0MEAIMDwEBBFRleHQBG9Cg0L7Qt9C60LvQsNC0INC30LDQvdGP0YLRjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALVdjzppTCyUtNVSyV7xykGQzHz2&__EVENTTARGET=&__EVENTARGUMENT=&ctl00%24MainContent%24ctl00%24txtboxGroup=\(groupName)&ctl00%24MainContent%24ctl00%24btnShowSchedule=%D0%A0%D0%BE%D0%B7%D0%BA%D0%BB%D0%B0%D0%B4%2B%D0%B7%D0%B0%D0%BD%D1%8F%D1%82%D1%8C&__EVENTVALIDATION=%2FwEdAAEAAAD%2F%2F%2F%2F%2FAQAAAAAAAAAPAQAAAAUAAAAIsA3rWl3AM%2B6E94I5Tu9cRJoVjv0LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHfLZVQO6kVoZVPGurJN4JJIAuaU&hiddenInputToUpdateATBuffer_CommonToolkitScripts=0" } diff --git a/Sources/App/Controllers/RozkladControllerV2.swift b/Sources/App/Controllers/RozkladControllerV2.swift new file mode 100644 index 0000000..d1308b8 --- /dev/null +++ b/Sources/App/Controllers/RozkladControllerV2.swift @@ -0,0 +1,51 @@ +// +// GroupsControllerV2.swift +// +// +// Created by Denys Danyliuk on 01.07.2022. +// + +import Vapor +import Routes +import KPIHubParser +import Foundation + +final class RozkladControllerV2 { + + func allGroups(request: Request) async throws -> GroupsResponseV2 { + let response: ClientResponse = try await request.client.get( + "https://schedule.kpi.ua/api/schedule/groups" + ) + let result = try response.content.decode(GroupModelV2ClientResponse.self) + return GroupsResponseV2( + numberOfGroups: result.data.count, + groups: result.data.sorted(by: { + $0.name.compare($1.name, locale: Locale(identifier: "uk")) == .orderedAscending + }) + ) + } + + func search(request: Request, searchQuery: GroupSearchQuery) async throws -> GroupV2 { + let allGroups = try await allGroups(request: request) + let searchedGroup = allGroups.groups.first { + $0.name.lowercased().contains(searchQuery.groupName.lowercased()) + } + if let searchedGroup = searchedGroup { + return searchedGroup + } else { + throw Abort(.notFound, reason: "Group not found") + } + } + + func getLessons(for groupUUID: UUID, request: Request) async throws -> LessonsResponseV2 { + let response: ClientResponse = try await request.client.get( + "https://schedule.kpi.ua/api/schedule/lessons", + beforeSend: { request in + try request.query.encode(["groupId": groupUUID.uuidString]) + } + ) + let lessonsV2ClientResponse = try response.content.decode(LessonsV2ClientResponse.self) + return LessonsResponseV2(id: groupUUID, lessons: lessonsV2ClientResponse.lessonsV2()) + } + +} diff --git a/Sources/App/Cron/RefreshGroupsCron.swift b/Sources/App/Cron/RefreshGroupsCron.swift index 745fd74..c4020bd 100644 --- a/Sources/App/Cron/RefreshGroupsCron.swift +++ b/Sources/App/Cron/RefreshGroupsCron.swift @@ -18,7 +18,7 @@ public struct RefreshGroupsCron: AsyncVaporCronSchedulable { public static func task(on application: Application) async throws -> Void { application.logger.info("\(Self.self) is running...") - let groupsController = GroupsController() + let groupsController = RozkladController() let groups = try await groupsController.getNewGroups( client: application.client, logger: application.logger diff --git a/Sources/App/Models/RozkladV2/ClientResponse/GroupModelV2ClientResponse.swift b/Sources/App/Models/RozkladV2/ClientResponse/GroupModelV2ClientResponse.swift new file mode 100644 index 0000000..41c8583 --- /dev/null +++ b/Sources/App/Models/RozkladV2/ClientResponse/GroupModelV2ClientResponse.swift @@ -0,0 +1,14 @@ +// +// GroupModelV2ClientResponse.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +struct GroupModelV2ClientResponse: Content { + + var data: [GroupV2] + +} diff --git a/Sources/App/Models/RozkladV2/ClientResponse/LessonsV2ClientResponse.swift b/Sources/App/Models/RozkladV2/ClientResponse/LessonsV2ClientResponse.swift new file mode 100644 index 0000000..e024656 --- /dev/null +++ b/Sources/App/Models/RozkladV2/ClientResponse/LessonsV2ClientResponse.swift @@ -0,0 +1,83 @@ +// +// LessonsV2ClientResponse.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +struct LessonsV2ClientResponse: Content { + + let data: Data + + struct Data: Content { + let scheduleFirstWeek: [Day] + let scheduleSecondWeek: [Day] + } + + struct Day: Content { + let day: String + let pairs: [Pair] + } + + struct Pair: Content { + let teacherName: String + let lecturerId: String + let type: String + let time: String + let name: String + let place: String + let tag: String + } + +} + +extension LessonsV2ClientResponse { + + func lessonsV2() -> [LessonV2] { + let firstWeek = getWeekLessons(from: data.scheduleFirstWeek, week: .first) + let secondWeek = getWeekLessons(from: data.scheduleSecondWeek, week: .second) + return firstWeek + secondWeek + } + + private func getWeekLessons(from days: [LessonsV2ClientResponse.Day], week: LessonV2.Week) -> [LessonV2] { + days + .enumerated() + .flatMap { index, day -> [LessonV2] in + day.pairs + .reduce(into: []) { partialResult, pair in + let firstIndex = partialResult.firstIndex(where: { $0.position.description.contains(pair.time) }) + if let firstIndex = firstIndex { + var old = partialResult.remove(at: firstIndex) + if !old.names.contains(pair.name) { + old.names.append(pair.name) + } + if !(old.teachers?.contains(pair.teacherName) ?? false) { + old.teachers?.append(pair.teacherName) + } + if !(old.locations?.contains(pair.place) ?? false) { + old.locations?.append(pair.place) + } + partialResult.insert(old, at: firstIndex) + + } else { + partialResult.append( + LessonV2( + names: [pair.name], + teachers: [pair.teacherName], + locations: [pair.place], + type: pair.type, + position: .init(pair.time), + day: LessonV2.Day(rawValue: index + 1) ?? .monday, + week: week + ) + ) + } + } + .sorted { lhs, rhs in + lhs.position.rawValue < rhs.position.rawValue + } + } + } +} diff --git a/Sources/App/Models/RozkladV2/GroupV2.swift b/Sources/App/Models/RozkladV2/GroupV2.swift new file mode 100644 index 0000000..dcf0491 --- /dev/null +++ b/Sources/App/Models/RozkladV2/GroupV2.swift @@ -0,0 +1,16 @@ +// +// GroupV2.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +struct GroupV2: Content { + + var id: UUID? + var name: String + var faculty: String? + +} diff --git a/Sources/App/Models/RozkladV2/GroupsResponseV2.swift b/Sources/App/Models/RozkladV2/GroupsResponseV2.swift new file mode 100644 index 0000000..dac0c1c --- /dev/null +++ b/Sources/App/Models/RozkladV2/GroupsResponseV2.swift @@ -0,0 +1,15 @@ +// +// GroupsResponseV2.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +struct GroupsResponseV2: Content { + + let numberOfGroups: Int + let groups: [GroupV2] + +} diff --git a/Sources/App/Models/RozkladV2/LessonV2.swift b/Sources/App/Models/RozkladV2/LessonV2.swift new file mode 100644 index 0000000..b6bb1d1 --- /dev/null +++ b/Sources/App/Models/RozkladV2/LessonV2.swift @@ -0,0 +1,103 @@ +// +// LessonV2.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +public struct LessonV2: Equatable { + + // MARK: - Position + + public enum Position: Int, Codable, CaseIterable, Equatable { + case first = 1 + case second + case third + case fourth + case fifth + case sixth + + init(_ string: String) { + switch string { + case "8.30": + self = .first + + case "10.25": + self = .second + + case "12.20": + self = .third + + case "14.15": + self = .fourth + + case "16.10": + self = .fifth + + case "18.30", "18.05": + self = .sixth + + default: + self = .first + } + } + + var description: [String] { + switch self { + case .first: + return ["8.30"] + + case .second: + return ["10.25"] + + case .third: + return ["12.20"] + + case .fourth: + return ["14.15"] + + case .fifth: + return ["16.10"] + + case .sixth: + return ["18.30", "18.05"] + } + } + } + + // MARK: - Day + + public enum Day: Int, Codable, CaseIterable, Equatable { + case monday = 1 + case tuesday + case wednesday + case thursday + case friday + case saturday + } + + // MARK: - Week + + public enum Week: Int, Codable, Equatable { + case first = 1 + case second + } + + public var names: [String] + public var teachers: [String]? + public var locations: [String]? + public var type: String + + public let position: Position + public let day: Day + public let week: Week + +} + +// MARK: - Codable + +extension LessonV2: Codable { + +} diff --git a/Sources/App/Models/RozkladV2/LessonsResponseV2.swift b/Sources/App/Models/RozkladV2/LessonsResponseV2.swift new file mode 100644 index 0000000..fe772d9 --- /dev/null +++ b/Sources/App/Models/RozkladV2/LessonsResponseV2.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Denys Danyliuk on 02.07.2022. +// + +import Vapor + +struct LessonsResponseV2 { + var id: UUID + let lessons: [LessonV2] +} + +extension LessonsResponseV2: Codable { + +} + +extension LessonsResponseV2: Content { + +} diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 8755d65..c4041bb 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -7,6 +7,9 @@ import VaporCron public func configure(_ app: Application) throws { + app.http.server.configuration.hostname = "0.0.0.0" + app.http.server.configuration.port = 8080 + app.logger.notice("env \(app.environment)") app.logger.notice("host \(Environment.get("DATABASE_HOST") ?? "no value")") app.logger.notice("port \(Environment.get("DATABASE_PORT") ?? "no value")") diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index ff9bdc0..d47c3b1 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -60,15 +60,15 @@ func groupsHandler( ) async throws -> AsyncResponseEncodable { switch route { case .all: - let controller = GroupsController() + let controller = RozkladControllerV2() return try await controller.allGroups(request: request) case let .search(searchQuery): - let controller = GroupsController() + let controller = RozkladControllerV2() return try await controller.search(request: request, searchQuery: searchQuery) case .forceRefresh: - let controller = GroupsController() + let controller = RozkladController() return try await controller.forceRefresh(request: request) } } @@ -82,7 +82,7 @@ func groupHandler( ) async throws -> AsyncResponseEncodable { switch route { case .lessons: - let controller = LessonsController() + let controller = RozkladControllerV2() return try await controller.getLessons(for: uuid, request: request) } } diff --git a/Sources/KPIHubParser/Models/Lesson.swift b/Sources/KPIHubParser/Models/Lesson.swift index 29d623d..4cd6283 100644 --- a/Sources/KPIHubParser/Models/Lesson.swift +++ b/Sources/KPIHubParser/Models/Lesson.swift @@ -39,8 +39,9 @@ public struct Lesson: Equatable { } public let names: [String] - public let teachers: [Teacher]? + public let teachers: [String]? public let locations: [String]? + public let type: String public let position: Position public let day: Day @@ -55,6 +56,7 @@ public struct Lesson: Equatable { self.names = rawLesson.names self.teachers = rawLesson.teachers self.locations = rawLesson.locations + self.type = rawLesson.type self.day = day self.week = week self.position = position diff --git a/Sources/KPIHubParser/Models/RawLesson.swift b/Sources/KPIHubParser/Models/RawLesson.swift index ce541bc..b74260a 100644 --- a/Sources/KPIHubParser/Models/RawLesson.swift +++ b/Sources/KPIHubParser/Models/RawLesson.swift @@ -10,8 +10,32 @@ import Foundation struct RawLesson: Equatable { let names: [String] - let teachers: [Teacher]? + let teachers: [String]? let locations: [String]? + let type: String + + init(names: [String], teachers: [Teacher]?, locations: [String]?) { + self.names = names + self.teachers = teachers?.map { $0.shortName } + self.locations = locations + self.type = RawLesson.type(for: locations?.first) + } + + static func type(for location: String?) -> String { + switch location?.lowercased() ?? "" { + case let string where string.contains("лек"): + return "Лекція" + + case let string where string.contains("прак"): + return "Практика" + + case let string where string.contains("лаб"): + return "Лабораторна" + + default: + return "Невідомо" + } + } }