mirror of
https://github.com/Xevion/banner.git
synced 2025-12-15 02:11:07 -06:00
refactor: rearrange & rename files, fix meeting times response decode
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"banner/internal"
|
||||
"banner/internal/config"
|
||||
"banner/internal/models"
|
||||
"banner/internal/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -51,7 +51,7 @@ func SessionMiddleware(c *resty.Client, r *resty.Response) error {
|
||||
// GenerateSession generates a new session ID (nonce) for use with the Banner API.
|
||||
// Don't use this function directly, use GetSession instead.
|
||||
func GenerateSession() string {
|
||||
return utils.RandomString(5) + utils.Nonce()
|
||||
return internal.RandomString(5) + internal.Nonce()
|
||||
}
|
||||
|
||||
var terms []BannerTerm
|
||||
@@ -131,7 +131,7 @@ func (a *API) GetTerms(search string, page int, maxResults int) ([]BannerTerm, e
|
||||
SetQueryParam("searchTerm", search).
|
||||
SetQueryParam("offset", strconv.Itoa(page)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]BannerTerm{})
|
||||
|
||||
@@ -206,7 +206,7 @@ func (a *API) GetPartOfTerms(search string, term int, offset int, maxResults int
|
||||
SetQueryParam("offset", strconv.Itoa(offset)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("uniqueSessionId", a.EnsureSession()).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]BannerTerm{})
|
||||
|
||||
@@ -239,7 +239,7 @@ func (a *API) GetInstructors(search string, term string, offset int, maxResults
|
||||
SetQueryParam("offset", strconv.Itoa(offset)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("uniqueSessionId", a.EnsureSession()).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]Instructor{})
|
||||
|
||||
@@ -337,7 +337,7 @@ func (a *API) GetSubjects(search string, term string, offset int, maxResults int
|
||||
SetQueryParam("offset", strconv.Itoa(offset)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("uniqueSessionId", a.EnsureSession()).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]Pair{})
|
||||
|
||||
@@ -370,7 +370,7 @@ func (a *API) GetCampuses(search string, term int, offset int, maxResults int) (
|
||||
SetQueryParam("offset", strconv.Itoa(offset)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("uniqueSessionId", a.EnsureSession()).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]Pair{})
|
||||
|
||||
@@ -403,7 +403,7 @@ func (a *API) GetInstructionalMethods(search string, term string, offset int, ma
|
||||
SetQueryParam("offset", strconv.Itoa(offset)).
|
||||
SetQueryParam("max", strconv.Itoa(maxResults)).
|
||||
SetQueryParam("uniqueSessionId", a.EnsureSession()).
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]Pair{})
|
||||
|
||||
@@ -423,23 +423,27 @@ func (a *API) GetInstructionalMethods(search string, term string, offset int, ma
|
||||
// It makes an HTTP GET request to the appropriate API endpoint and parses the response to extract the meeting time data.
|
||||
// The function returns a MeetingTimeResponse struct containing the extracted information.
|
||||
func (a *API) GetCourseMeetingTime(term int, crn int) ([]models.MeetingTimeResponse, error) {
|
||||
type responseWrapper struct {
|
||||
Fmt []models.MeetingTimeResponse `json:"fmt"`
|
||||
}
|
||||
|
||||
req := a.config.Client.NewRequest().
|
||||
SetQueryParam("term", strconv.Itoa(term)).
|
||||
SetQueryParam("courseReferenceNumber", strconv.Itoa(crn)).
|
||||
SetExpectResponseContentType("application/json").
|
||||
SetResult(&[]models.MeetingTimeResponse{})
|
||||
SetResult(&responseWrapper{})
|
||||
|
||||
res, err := req.Get("/searchResults/getFacultyMeetingTimes")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get meeting time: %w", err)
|
||||
}
|
||||
|
||||
meetingTimes, ok := res.Result().(*[]models.MeetingTimeResponse)
|
||||
result, ok := res.Result().(*responseWrapper)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("meeting times parsing failed to cast: %v", res.Result())
|
||||
}
|
||||
|
||||
return *meetingTimes, nil
|
||||
return result.Fmt, nil
|
||||
}
|
||||
|
||||
// ResetDataForm makes a POST request that needs to be made upon before new search requests can be made.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"banner/internal"
|
||||
"banner/internal/models"
|
||||
"banner/internal/utils"
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
@@ -70,7 +70,7 @@ func (a *API) Scrape() error {
|
||||
|
||||
// GetExpiredSubjects returns a list of subjects that are expired and should be scraped.
|
||||
func (a *API) GetExpiredSubjects() ([]string, error) {
|
||||
term := utils.Default(time.Now()).ToString()
|
||||
term := Default(time.Now()).ToString()
|
||||
subjects := make([]string, 0)
|
||||
|
||||
// Create a timeout context for Redis operations
|
||||
@@ -109,7 +109,7 @@ func (a *API) ScrapeMajor(subject string) error {
|
||||
for {
|
||||
// Build & execute the query
|
||||
query := NewQuery().Offset(offset).MaxResults(MaxPageSize * 2).Subject(subject)
|
||||
term := utils.Default(time.Now()).ToString()
|
||||
term := Default(time.Now()).ToString()
|
||||
result, err := a.Search(term, query, "subjectDescription", false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("search failed: %w (%s)", err, query.String())
|
||||
@@ -152,7 +152,7 @@ func (a *API) ScrapeMajor(subject string) error {
|
||||
break
|
||||
}
|
||||
|
||||
term := utils.Default(time.Now()).ToString()
|
||||
term := Default(time.Now()).ToString()
|
||||
|
||||
// Calculate the expiry time for the scrape (1 hour for every 200 classes, random +-15%) with a minimum of 1 hour
|
||||
var scrapeExpiry time.Duration
|
||||
@@ -190,7 +190,7 @@ func (a *API) CalculateExpiry(term string, count int, priority bool) time.Durati
|
||||
// Subjects with less than 50 classes have a reversed expiry (less classes, longer interval)
|
||||
// 1 class => 12 hours, 49 classes => 1 hour
|
||||
if count < 50 {
|
||||
hours := utils.Slope(utils.Point{X: 1, Y: 12}, utils.Point{X: 49, Y: 1}, float64(count)).Y
|
||||
hours := internal.Slope(internal.Point{X: 1, Y: 12}, internal.Point{X: 49, Y: 1}, float64(count)).Y
|
||||
baseExpiry = time.Duration(hours * float64(time.Hour))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"banner/internal/utils"
|
||||
"banner/internal"
|
||||
"net/url"
|
||||
|
||||
log "github.com/rs/zerolog/log"
|
||||
@@ -18,7 +18,7 @@ func (a *API) Setup() {
|
||||
|
||||
for _, path := range requestQueue {
|
||||
req := a.config.Client.NewRequest().
|
||||
SetQueryParam("_", utils.Nonce()).
|
||||
SetQueryParam("_", internal.Nonce()).
|
||||
SetExpectResponseContentType("application/json")
|
||||
|
||||
res, err := req.Get(path)
|
||||
|
||||
158
internal/api/term.go
Normal file
158
internal/api/term.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"banner/internal/config"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Term selection should yield smart results based on the current time, as well as the input provided.
|
||||
// Fall 2024, "spring" => Spring 2025
|
||||
// Fall 2024, "fall" => Fall 2025
|
||||
// Summer 2024, "fall" => Fall 2024
|
||||
|
||||
const (
|
||||
Spring = iota
|
||||
Summer
|
||||
Fall
|
||||
)
|
||||
|
||||
type Term struct {
|
||||
Year uint16
|
||||
Season uint8
|
||||
}
|
||||
|
||||
var (
|
||||
SpringRange, SummerRange, FallRange YearDayRange
|
||||
)
|
||||
|
||||
func init() {
|
||||
loc, _ := time.LoadLocation(config.CentralTimezoneName)
|
||||
SpringRange, SummerRange, FallRange = GetYearDayRange(loc, uint16(time.Now().Year()))
|
||||
}
|
||||
|
||||
type YearDayRange struct {
|
||||
Start uint16
|
||||
End uint16
|
||||
}
|
||||
|
||||
// GetYearDayRange returns the start and end day of each term for the given year.
|
||||
// This could technically introduce race conditions, but it's more likely confusion from UTC will be a greater issue.
|
||||
// Spring: January 14th to May
|
||||
// Summer: May 25th - August 15th
|
||||
// Fall: August 18th - December 10th
|
||||
func GetYearDayRange(loc *time.Location, year uint16) (YearDayRange, YearDayRange, YearDayRange) {
|
||||
springStart := time.Date(int(year), time.January, 14, 0, 0, 0, 0, loc).YearDay()
|
||||
springEnd := time.Date(int(year), time.May, 1, 0, 0, 0, 0, loc).YearDay()
|
||||
summerStart := time.Date(int(year), time.May, 25, 0, 0, 0, 0, loc).YearDay()
|
||||
summerEnd := time.Date(int(year), time.August, 15, 0, 0, 0, 0, loc).YearDay()
|
||||
fallStart := time.Date(int(year), time.August, 18, 0, 0, 0, 0, loc).YearDay()
|
||||
fallEnd := time.Date(int(year), time.December, 10, 0, 0, 0, 0, loc).YearDay()
|
||||
|
||||
return YearDayRange{
|
||||
Start: uint16(springStart),
|
||||
End: uint16(springEnd),
|
||||
}, YearDayRange{
|
||||
Start: uint16(summerStart),
|
||||
End: uint16(summerEnd),
|
||||
}, YearDayRange{
|
||||
Start: uint16(fallStart),
|
||||
End: uint16(fallEnd),
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentTerm returns the current term, and the next term. Only the first term is nillable.
|
||||
// YearDay ranges are inclusive of the start, and exclusive of the end.
|
||||
// You can think of the 'year' part of it as the 'school year', the second part of the 20XX-(20XX+1) phrasing.
|
||||
//
|
||||
// e.g. the Fall 2025, Spring 2026, and Summer 2026 terms all occur as part of the 2025-2026 school year. The second year, 2026, is the part used in all term identifiers.
|
||||
// So even though the Fall 2025 term occurs in 2025, it uses the 2026 year in it's term identifier.
|
||||
//
|
||||
// Fall of 2024 => 202510
|
||||
// Spring of 2025 => 202520
|
||||
// Summer of 2025 => 202530
|
||||
// Fall of 2025 => 202610
|
||||
// Spring of 2026 => 202620
|
||||
// Summer of 2026 => 202630
|
||||
//
|
||||
// Reading out 'Fall of 2024' as '202510' might be confusing, but it's correct.
|
||||
func GetCurrentTerm(now time.Time) (*Term, *Term) {
|
||||
literalYear := uint16(now.Year())
|
||||
dayOfYear := uint16(now.YearDay())
|
||||
|
||||
// If we're past the end of the summer term, we're 'in' the next school year.
|
||||
var termYear uint16
|
||||
if dayOfYear > SummerRange.End {
|
||||
termYear = literalYear + 1
|
||||
} else {
|
||||
termYear = literalYear
|
||||
}
|
||||
|
||||
if (dayOfYear < SpringRange.Start) || (dayOfYear >= FallRange.End) {
|
||||
// Fall over, Spring not yet begun
|
||||
return nil, &Term{Year: termYear, Season: Spring}
|
||||
} else if (dayOfYear >= SpringRange.Start) && (dayOfYear < SpringRange.End) {
|
||||
// Spring
|
||||
return &Term{Year: termYear, Season: Spring}, &Term{Year: termYear, Season: Summer}
|
||||
} else if dayOfYear < SummerRange.Start {
|
||||
// Spring over, Summer not yet begun
|
||||
return nil, &Term{Year: termYear, Season: Summer}
|
||||
} else if (dayOfYear >= SummerRange.Start) && (dayOfYear < SummerRange.End) {
|
||||
// Summer
|
||||
return &Term{Year: termYear, Season: Summer}, &Term{Year: termYear, Season: Fall}
|
||||
} else if dayOfYear < FallRange.Start {
|
||||
// Summer over, Fall not yet begun
|
||||
return nil, &Term{Year: termYear, Season: Fall}
|
||||
} else if (dayOfYear >= FallRange.Start) && (dayOfYear < FallRange.End) {
|
||||
// Fall
|
||||
return &Term{Year: termYear, Season: Fall}, nil
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Impossible Code Reached (dayOfYear: %d)", dayOfYear))
|
||||
}
|
||||
|
||||
// ParseTerm converts a Banner term code to a Term struct
|
||||
func ParseTerm(code string) Term {
|
||||
year, _ := strconv.ParseUint(code[0:4], 10, 16)
|
||||
|
||||
var season uint8
|
||||
termCode := code[4:6]
|
||||
switch termCode {
|
||||
case "10":
|
||||
season = Fall
|
||||
case "20":
|
||||
season = Spring
|
||||
case "30":
|
||||
season = Summer
|
||||
}
|
||||
|
||||
return Term{
|
||||
Year: uint16(year),
|
||||
Season: season,
|
||||
}
|
||||
}
|
||||
|
||||
// TermToBannerTerm converts a Term struct to a Banner term code
|
||||
func (term Term) ToString() string {
|
||||
var season string
|
||||
switch term.Season {
|
||||
case Fall:
|
||||
season = "10"
|
||||
case Spring:
|
||||
season = "20"
|
||||
case Summer:
|
||||
season = "30"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d%s", term.Year, season)
|
||||
}
|
||||
|
||||
// Default chooses the default term, which is the current term if it exists, otherwise the next term.
|
||||
func Default(t time.Time) Term {
|
||||
currentTerm, nextTerm := GetCurrentTerm(t)
|
||||
if currentTerm == nil {
|
||||
return *nextTerm
|
||||
}
|
||||
return *currentTerm
|
||||
}
|
||||
Reference in New Issue
Block a user