diff --git a/go/api/data/handler.go b/go/api/data/handler.go index 726e8fd..e652c4e 100644 --- a/go/api/data/handler.go +++ b/go/api/data/handler.go @@ -529,7 +529,7 @@ func (h *Handler) QuerySchedules(ctx context.Context, airline common.AirlineIden fn := fs.Number() if variant.Data.ServiceType == "J" && variant.Data.AircraftType == aircraftType && variant.Data.AircraftConfigurationVersion == aircraftConfigurationVersion && variant.Data.OperatedAs == fn { - if span, ok := variant.Ranges.Span(); ok { + if cnt, span := variant.Ranges.Span(); cnt > 0 { idx := slices.IndexFunc(result[fn], func(rr RouteAndRange) bool { return rr.DepartureAirport == variant.Data.DepartureAirport && rr.ArrivalAirport == variant.Data.ArrivalAirport }) @@ -558,6 +558,32 @@ func (h *Handler) QuerySchedules(ctx context.Context, airline common.AirlineIden }) } +func (h *Handler) FlightSchedules(ctx context.Context, airline common.AirlineIdentifier, fn func(seq iter.Seq[*common.FlightSchedule]) error) error { + return h.flightSchedules(ctx, airline, func(seq iter.Seq[jstream.KV]) error { + var internalErr error + err := fn(func(yield func(*common.FlightSchedule) bool) { + for kv := range seq { + var b []byte + b, internalErr = json.Marshal(kv.Value) + if internalErr != nil { + return + } + + var fs *common.FlightSchedule + if internalErr = json.Unmarshal(b, &fs); internalErr != nil { + return + } + + if !yield(fs) { + return + } + } + }) + + return errors.Join(internalErr, err) + }) +} + func (h *Handler) flightSchedules(ctx context.Context, airline common.AirlineIdentifier, fn func(seq iter.Seq[jstream.KV]) error) error { resp, err := h.s3c.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(h.bucket), diff --git a/go/api/main.go b/go/api/main.go index fb076cf..af42477 100644 --- a/go/api/main.go +++ b/go/api/main.go @@ -76,6 +76,8 @@ func main() { e.GET("/data/:airline/schedule/:aircraftType/:aircraftConfigurationVersion/v2", web.NewQueryFlightSchedulesEndpoint(dataHandler)) e.GET("/data/:fn/:departureDate/:departureAirport/feed.rss", web.NewFlightUpdateFeedEndpoint(dataHandler, "application/rss+xml", (*feeds.Feed).WriteRss)) e.GET("/data/:fn/:departureDate/:departureAirport/feed.atom", web.NewFlightUpdateFeedEndpoint(dataHandler, "application/atom+xml", (*feeds.Feed).WriteAtom)) + e.GET("/data/allegris/feed.rss", web.NewAllegrisUpdateFeedEndpoint(dataHandler, "application/rss+xml", (*feeds.Feed).WriteRss)) + e.GET("/data/allegris/feed.atom", web.NewAllegrisUpdateFeedEndpoint(dataHandler, "application/atom+xml", (*feeds.Feed).WriteAtom)) if err := run(ctx, e); err != nil { panic(err) diff --git a/go/api/web/feed.go b/go/api/web/feed.go index 770aa3d..0feec27 100644 --- a/go/api/web/feed.go +++ b/go/api/web/feed.go @@ -1,6 +1,7 @@ package web import ( + "cmp" "fmt" "github.com/explore-flights/monorepo/go/api/data" "github.com/explore-flights/monorepo/go/common" @@ -9,6 +10,7 @@ import ( "github.com/gorilla/feeds" "github.com/labstack/echo/v4" "io" + "iter" "maps" "net/http" "net/url" @@ -120,3 +122,142 @@ Codeshares: %+v return nil } } + +func NewAllegrisUpdateFeedEndpoint(dh *data.Handler, contentType string, writer func(*feeds.Feed, io.Writer) error) echo.HandlerFunc { + const ( + feedId = "https://explore.flights/allegris" + allegrisAircraftType = "359" + allegrisAircraftConfigurationNoFirst = "C38E24M201" + allegrisAircraftConfigurationWithFirst = "F4C38E24M201" + ) + + buildItemLink := func(fn common.FlightNumber, aircraftConfigurationVersion string) string { + q := make(url.Values) + q.Set("aircraft_type", allegrisAircraftType) + q.Set("aircraft_configuration_version", aircraftConfigurationVersion) + + return fmt.Sprintf("https://explore.flights/flight/%s?%s", fn.String(), q.Encode()) + } + + buildItemContent := func(fn common.FlightNumber, variants []*common.FlightScheduleVariant) string { + rangeByRoute := make(map[[2]string]xtime.LocalDateRanges) + + for _, variant := range variants { + route := [2]string{variant.Data.DepartureAirport, variant.Data.ArrivalAirport} + + if ldrs, ok := rangeByRoute[route]; ok { + rangeByRoute[route] = ldrs.ExpandAll(variant.Ranges) + } else { + rangeByRoute[route] = variant.Ranges + } + } + + routesSorted := slices.SortedFunc(maps.Keys(rangeByRoute), func(a [2]string, b [2]string) int { + return cmp.Or( + cmp.Compare(a[0], b[0]), + cmp.Compare(a[1], b[1]), + ) + }) + + result := fmt.Sprintf("Flight %s operates Allegris on:\n", fn.String()) + for _, route := range routesSorted { + ldrs := rangeByRoute[route] + if cnt, span := ldrs.Span(); cnt > 0 { + result += fmt.Sprintf("%s - %s from %s until %s (%d days)\n", route[0], route[1], span[0].String(), span[1].String(), cnt) + } + } + + return strings.TrimSpace(result) + } + + return func(c echo.Context) error { + results := make(map[string]map[common.FlightNumber][]*common.FlightScheduleVariant) + + err := dh.FlightSchedules(c.Request().Context(), common.Lufthansa, func(seq iter.Seq[*common.FlightSchedule]) error { + for fs := range seq { + fn := fs.Number() + + for _, variant := range fs.Variants { + if variant.Data.ServiceType == "J" && variant.Data.AircraftType == allegrisAircraftType && variant.Data.OperatedAs == fn { + if variant.Data.AircraftConfigurationVersion == allegrisAircraftConfigurationNoFirst || variant.Data.AircraftConfigurationVersion == allegrisAircraftConfigurationWithFirst { + byFn, ok := results[variant.Data.AircraftConfigurationVersion] + if !ok { + byFn = make(map[common.FlightNumber][]*common.FlightScheduleVariant) + results[variant.Data.AircraftConfigurationVersion] = byFn + } + + byFn[fn] = append(byFn[fn], variant) + } + } + } + } + + return nil + }) + + if err != nil { + noCache(c) + return echo.NewHTTPError(http.StatusInternalServerError) + } + + feed := &feeds.Feed{ + Id: feedId, + Title: "Lufthansa Allegris Flights", + Link: &feeds.Link{ + Href: feedId, + Rel: "self", + Type: "text/html", + }, + } + + for aircraftConfigurationVersion, byFn := range results { + for fn, variants := range byFn { + if len(variants) < 0 { + continue + } + + itemLink := buildItemLink(fn, aircraftConfigurationVersion) + var created time.Time + var updated time.Time + + for _, variant := range variants { + if created.IsZero() || created.After(variant.Metadata.CreationTime) { + created = variant.Metadata.CreationTime + } + + updateTime := common.Max(variant.Metadata.RangesUpdateTime, variant.Metadata.DataUpdateTime) + if updated.IsZero() || updated.Before(updateTime) { + updated = updateTime + } + } + + var suffix string + if aircraftConfigurationVersion == allegrisAircraftConfigurationNoFirst { + suffix = "without first" + } else { + suffix = "with first" + } + + content := buildItemContent(fn, variants) + feed.Items = append(feed.Items, &feeds.Item{ + Id: itemLink, + IsPermaLink: "false", + Title: fmt.Sprintf("Flight %s operates on Allegris %s (%s)", fn.String(), suffix, aircraftConfigurationVersion), + Link: &feeds.Link{ + Href: itemLink, + Rel: "self", + Type: "text/html", + }, + Created: created, + Updated: updated, + Content: content, + Description: content, + }) + } + } + + c.Response().Header().Add(echo.HeaderContentType, contentType) + addExpirationHeaders(c, time.Now(), time.Hour) + return writer(feed, c.Response()) + } +} diff --git a/go/common/util.go b/go/common/util.go new file mode 100644 index 0000000..d235420 --- /dev/null +++ b/go/common/util.go @@ -0,0 +1,21 @@ +package common + +type SelfComparator[T any] interface { + Compare(other T) int +} + +func Min[T SelfComparator[T]](a, b T) T { + if a.Compare(b) < 0 { + return a + } + + return b +} + +func Max[T SelfComparator[T]](a, b T) T { + if a.Compare(b) > 0 { + return a + } + + return b +} diff --git a/go/common/xtime/range.go b/go/common/xtime/range.go index 13845cf..28b3fc9 100644 --- a/go/common/xtime/range.go +++ b/go/common/xtime/range.go @@ -94,13 +94,13 @@ func (ldrs LocalDateRanges) Iter() iter.Seq[LocalDate] { } } -func (ldrs LocalDateRanges) Span() (LocalDateRange, bool) { +func (ldrs LocalDateRanges) Span() (int, LocalDateRange) { sorted := slices.SortedFunc(ldrs.Iter(), LocalDate.Compare) if len(sorted) < 1 { - return LocalDateRange{}, false + return 0, LocalDateRange{} } - return LocalDateRange{sorted[0], sorted[len(sorted)-1]}, true + return len(sorted), LocalDateRange{sorted[0], sorted[len(sorted)-1]} } func (ldrs LocalDateRanges) ExpandAll(other LocalDateRanges) LocalDateRanges { diff --git a/go/common/xtime/util.go b/go/common/xtime/util.go deleted file mode 100644 index 458ecb5..0000000 --- a/go/common/xtime/util.go +++ /dev/null @@ -1,19 +0,0 @@ -package xtime - -import "time" - -func Min(a, b time.Time) time.Time { - if a.Before(b) { - return a - } - - return b -} - -func Max(a, b time.Time) time.Time { - if a.After(b) { - return a - } - - return b -} diff --git a/go/cron/action/convert_flight_schedules.go b/go/cron/action/convert_flight_schedules.go index 379f89b..43bb179 100644 --- a/go/cron/action/convert_flight_schedules.go +++ b/go/cron/action/convert_flight_schedules.go @@ -348,10 +348,10 @@ func convertFlightSchedulesToFlights(queryDate xtime.LocalDate, lastModified tim } func combineFlights(f, other *common.Flight) { - f.Metadata.CreationTime = xtime.Min(f.Metadata.CreationTime, other.Metadata.CreationTime) + f.Metadata.CreationTime = common.Min(f.Metadata.CreationTime, other.Metadata.CreationTime) if f.DataEqual(other) { - f.Metadata.UpdateTime = xtime.Min(f.Metadata.UpdateTime, other.Metadata.UpdateTime) + f.Metadata.UpdateTime = common.Min(f.Metadata.UpdateTime, other.Metadata.UpdateTime) for codeShareFn, codeShare := range f.CodeShares { f.CodeShares[codeShareFn] = combineCodeShares(codeShare, other.CodeShares[codeShareFn]) @@ -392,10 +392,10 @@ func combineFlights(f, other *common.Flight) { } func combineCodeShares(a, b common.CodeShare) common.CodeShare { - a.Metadata.CreationTime = xtime.Min(a.Metadata.CreationTime, b.Metadata.CreationTime) + a.Metadata.CreationTime = common.Min(a.Metadata.CreationTime, b.Metadata.CreationTime) if a.DataEqual(b) { - a.Metadata.UpdateTime = xtime.Min(a.Metadata.UpdateTime, b.Metadata.UpdateTime) + a.Metadata.UpdateTime = common.Min(a.Metadata.UpdateTime, b.Metadata.UpdateTime) } else { bIsNewer := a.Metadata.UpdateTime.Before(b.Metadata.UpdateTime) if bIsNewer { diff --git a/go/cron/action/convert_flights.go b/go/cron/action/convert_flights.go index ce8f87b..0bb0e9a 100644 --- a/go/cron/action/convert_flights.go +++ b/go/cron/action/convert_flights.go @@ -82,9 +82,9 @@ func (a *cfAction) convertAll(ctx context.Context, bucket, prefix string, dateRa if fs, ok := byFlightNumber[fn]; ok { if variant, ok := fs.Variant(fsd); ok { variant.Ranges = variant.Ranges.Add(d) - variant.Metadata.CreationTime = xtime.Min(variant.Metadata.CreationTime, md.CreationTime) - variant.Metadata.RangesUpdateTime = xtime.Max(variant.Metadata.RangesUpdateTime, md.RangesUpdateTime) - variant.Metadata.DataUpdateTime = xtime.Min(variant.Metadata.DataUpdateTime, md.DataUpdateTime) + variant.Metadata.CreationTime = common.Min(variant.Metadata.CreationTime, md.CreationTime) + variant.Metadata.RangesUpdateTime = common.Max(variant.Metadata.RangesUpdateTime, md.RangesUpdateTime) + variant.Metadata.DataUpdateTime = common.Min(variant.Metadata.DataUpdateTime, md.DataUpdateTime) } else { fs.Variants = append(fs.Variants, &common.FlightScheduleVariant{ Ranges: xtime.NewLocalDateRanges(xiter.Single(d)), @@ -283,11 +283,11 @@ func combineSchedules(fs *common.FlightSchedule, existing *common.FlightSchedule if variant, ok := fs.Variant(existingVariant.Data); ok { if len(existingVariant.Ranges) > 0 { variant.Ranges = variant.Ranges.ExpandAll(existingVariant.Ranges) - variant.Metadata.RangesUpdateTime = xtime.Max(variant.Metadata.RangesUpdateTime, existingVariant.Metadata.RangesUpdateTime) + variant.Metadata.RangesUpdateTime = common.Max(variant.Metadata.RangesUpdateTime, existingVariant.Metadata.RangesUpdateTime) } - variant.Metadata.CreationTime = xtime.Min(variant.Metadata.CreationTime, existingVariant.Metadata.CreationTime) - variant.Metadata.DataUpdateTime = xtime.Min(variant.Metadata.DataUpdateTime, existingVariant.Metadata.DataUpdateTime) + variant.Metadata.CreationTime = common.Min(variant.Metadata.CreationTime, existingVariant.Metadata.CreationTime) + variant.Metadata.DataUpdateTime = common.Min(variant.Metadata.DataUpdateTime, existingVariant.Metadata.DataUpdateTime) } else if len(variant.Ranges) > 0 { fs.Variants = append(fs.Variants, existingVariant) }