- commit
- 7bab30e3626558ee33a180bd2c2ca3c244c772ae
- author
- Dami <dami.etoile@gmail.com>
| A | go.mod | 37 | +++ |
| A | go.sum | 70 | ++++++ |
| A | internal/ | 111 | +++++++++ |
| A | internal/ | 194 | ++++++++++++++++ |
| A | internal/ | 180 | +++++++++++++++ |
| A | internal/ | 266 | ++++++++++++++++++++++ |
| A | internal/ | 95 | ++++++++ |
| A | main.go | 36 | +++ |
| A | schema.md | 365 | ++++++++++++++++++++++++++++++ |
| A | showings | bin |
@@ -0,0 +1,37 @@
1+module github.com/dami/showings2+3+go 1.24.24+5+require (6+ github.com/charmbracelet/bubbles v1.0.07+ github.com/charmbracelet/bubbletea v1.3.108+ github.com/charmbracelet/lipgloss v1.1.09+ github.com/spf13/cobra v1.8.110+)11+12+require (13+ github.com/atotto/clipboard v0.1.4 // indirect14+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect15+ github.com/charmbracelet/colorprofile v0.4.1 // indirect16+ github.com/charmbracelet/x/ansi v0.11.6 // indirect17+ github.com/charmbracelet/x/cellbuf v0.0.15 // indirect18+ github.com/charmbracelet/x/term v0.2.2 // indirect19+ github.com/clipperhouse/displaywidth v0.9.0 // indirect20+ github.com/clipperhouse/stringish v0.1.1 // indirect21+ github.com/clipperhouse/uax29/v2 v2.5.0 // indirect22+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect23+ github.com/inconshreveable/mousetrap v1.1.0 // indirect24+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect25+ github.com/mattn/go-isatty v0.0.20 // indirect26+ github.com/mattn/go-localereader v0.0.1 // indirect27+ github.com/mattn/go-runewidth v0.0.19 // indirect28+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect29+ github.com/muesli/cancelreader v0.2.2 // indirect30+ github.com/muesli/termenv v0.16.0 // indirect31+ github.com/rivo/uniseg v0.4.7 // indirect32+ github.com/sahilm/fuzzy v0.1.1 // indirect33+ github.com/spf13/pflag v1.0.5 // indirect34+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect35+ golang.org/x/sys v0.38.0 // indirect36+ golang.org/x/text v0.3.8 // indirect37+)@@ -0,0 +1,70 @@
1+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=2+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=3+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=4+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=5+github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=6+github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=7+github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=8+github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=9+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=10+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=11+github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=12+github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=13+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=14+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=15+github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=16+github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=17+github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=18+github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=19+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=20+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=21+github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=22+github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=23+github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=24+github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=25+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=26+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=27+github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=28+github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=29+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=30+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=31+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=32+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=33+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=34+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=35+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=36+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=37+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=38+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=39+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=40+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=41+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=42+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=43+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=44+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=45+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=46+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=47+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=48+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=49+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=50+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=51+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=52+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=53+github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=54+github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=55+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=56+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=57+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=58+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=59+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=60+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=61+golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=62+golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=63+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=64+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=65+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=66+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=67+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=68+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=69+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=70+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=@@ -0,0 +1,111 @@
1+package api2+3+import (4+ "encoding/json"5+ "fmt"6+ "net/http"7+ "time"8+9+ "github.com/dami/showings/internal/models"10+)11+12+const defaultBaseURL = "https://drafthouse.com/s/mother/v2/schedule/venue/"13+14+type Client struct {15+ httpClient *http.Client16+ baseURL string17+}18+19+func NewClient() *Client {20+ return &Client{21+ httpClient: &http.Client{22+ Timeout: 30 * time.Second,23+ },24+ baseURL: defaultBaseURL,25+ }26+}27+28+func NewClientWithURL(baseURL string) *Client {29+ return &Client{30+ httpClient: &http.Client{31+ Timeout: 30 * time.Second,32+ },33+ baseURL: baseURL,34+ }35+}36+37+func (c *Client) FetchSchedule(venue string) (*models.APIResponse, error) {38+ url := c.baseURL + venue39+ resp, err := c.httpClient.Get(url)40+ if err != nil {41+ return nil, fmt.Errorf("failed to fetch schedule: %w", err)42+ }43+ defer resp.Body.Close()44+45+ if resp.StatusCode != http.StatusOK {46+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)47+ }48+49+ var result models.APIResponse50+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {51+ return nil, fmt.Errorf("failed to decode response: %w", err)52+ }53+54+ return &result, nil55+}56+57+func (c *Client) FilterUpcomingPresentations(data *models.Data) []models.Presentation {58+ upcomingSlugs := make(map[string]bool)59+ for _, session := range data.Sessions {60+ if session.Status == "ONSALE" {61+ upcomingSlugs[session.PresentationSlug] = true62+ }63+ }64+65+ var presentations []models.Presentation66+ for _, p := range data.Presentations {67+ if upcomingSlugs[p.Slug] && !p.IsHidden {68+ presentations = append(presentations, p)69+ }70+ }71+ return presentations72+}73+74+func (c *Client) GetSessionsForPresentation(data *models.Data, slug string) []models.Session {75+ var sessions []models.Session76+ for _, s := range data.Sessions {77+ if s.PresentationSlug == slug && s.Status == "ONSALE" {78+ sessions = append(sessions, s)79+ }80+ }81+ return sessions82+}83+84+func (c *Client) GetCinema(data *models.Data, cinemaID string) *models.Cinema {85+ for _, market := range data.Market {86+ for _, cinema := range market.Cinemas {87+ if cinema.ID == cinemaID {88+ return &cinema89+ }90+ }91+ }92+ return nil93+}94+95+func (c *Client) GetFormat(data *models.Data, slug string) *models.Format {96+ for _, f := range data.Formats {97+ if f.Slug == slug {98+ return &f99+ }100+ }101+ return nil102+}103+104+func (c *Client) GetAgePolicy(data *models.Data, slug string) *models.AgePolicy {105+ for _, ap := range data.AgePolicies {106+ if ap.Slug == slug {107+ return &ap108+ }109+ }110+ return nil111+}@@ -0,0 +1,194 @@
1+package api2+3+import (4+ "encoding/json"5+ "net/http"6+ "net/http/httptest"7+ "testing"8+9+ "github.com/dami/showings/internal/models"10+)11+12+func TestFetchSchedule(t *testing.T) {13+ expectedData := models.Data{14+ Presentations: []models.Presentation{15+ {16+ Slug: "test-movie-2026",17+ Show: models.Show{18+ Title: "Test Movie (2026)",19+ Certification: "PG-13",20+ },21+ },22+ },23+ Sessions: []models.Session{24+ {25+ SessionId: "12345",26+ PresentationSlug: "test-movie-2026",27+ Status: "ONSALE",28+ BusinessDateClt: "2026-02-14",29+ ShowTimeClt: "2026-02-14T19:00:00",30+ CinemaId: "0403",31+ FormatSlug: "2d-digital",32+ AgePolicySlug: "rated-pg-13",33+ },34+ },35+ Formats: []models.Format{36+ {37+ Slug: "2d-digital",38+ Title: "2D Digital",39+ },40+ },41+ AgePolicies: []models.AgePolicy{42+ {43+ Slug: "rated-pg-13",44+ Name: "Rated PG-13",45+ },46+ },47+ Market: []models.Market{48+ {49+ ID: "0400",50+ Slug: "denver",51+ Cinemas: []models.Cinema{52+ {53+ ID: "0403",54+ Name: "Westminster",55+ City: "Westminster",56+ State: "CO",57+ },58+ },59+ },60+ },61+ }62+63+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {64+ w.Header().Set("Content-Type", "application/json")65+ json.NewEncoder(w).Encode(models.APIResponse{Data: expectedData})66+ }))67+ defer server.Close()68+69+ client := NewClientWithURL(server.URL + "/")70+71+ result, err := client.FetchSchedule("westminster")72+ if err != nil {73+ t.Fatalf("FetchSchedule failed: %v", err)74+ }75+76+ if len(result.Data.Presentations) != 1 {77+ t.Errorf("Expected 1 presentation, got %d", len(result.Data.Presentations))78+ }79+80+ if result.Data.Presentations[0].Show.Title != "Test Movie (2026)" {81+ t.Errorf("Expected title 'Test Movie (2026)', got '%s'", result.Data.Presentations[0].Show.Title)82+ }83+}84+85+func TestFilterUpcomingPresentations(t *testing.T) {86+ data := &models.Data{87+ Presentations: []models.Presentation{88+ {Slug: "movie-1", IsHidden: false},89+ {Slug: "movie-2", IsHidden: false},90+ {Slug: "movie-3", IsHidden: true},91+ },92+ Sessions: []models.Session{93+ {PresentationSlug: "movie-1", Status: "ONSALE"},94+ {PresentationSlug: "movie-2", Status: "PAST"},95+ {PresentationSlug: "movie-3", Status: "ONSALE"},96+ },97+ }98+99+ client := NewClient()100+ presentations := client.FilterUpcomingPresentations(data)101+102+ if len(presentations) != 1 {103+ t.Errorf("Expected 1 upcoming presentation, got %d", len(presentations))104+ }105+106+ if presentations[0].Slug != "movie-1" {107+ t.Errorf("Expected 'movie-1', got '%s'", presentations[0].Slug)108+ }109+}110+111+func TestGetSessionsForPresentation(t *testing.T) {112+ data := &models.Data{113+ Sessions: []models.Session{114+ {PresentationSlug: "movie-1", Status: "ONSALE", SessionId: "1"},115+ {PresentationSlug: "movie-1", Status: "ONSALE", SessionId: "2"},116+ {PresentationSlug: "movie-1", Status: "PAST", SessionId: "3"},117+ {PresentationSlug: "movie-2", Status: "ONSALE", SessionId: "4"},118+ },119+ }120+121+ client := NewClient()122+ sessions := client.GetSessionsForPresentation(data, "movie-1")123+124+ if len(sessions) != 2 {125+ t.Errorf("Expected 2 sessions for movie-1, got %d", len(sessions))126+ }127+}128+129+func TestGetCinema(t *testing.T) {130+ data := &models.Data{131+ Market: []models.Market{132+ {133+ Cinemas: []models.Cinema{134+ {ID: "0403", Name: "Westminster"},135+ },136+ },137+ },138+ }139+140+ client := NewClient()141+ cinema := client.GetCinema(data, "0403")142+143+ if cinema == nil {144+ t.Fatal("Expected cinema, got nil")145+ }146+147+ if cinema.Name != "Westminster" {148+ t.Errorf("Expected 'Westminster', got '%s'", cinema.Name)149+ }150+151+ if client.GetCinema(data, "invalid") != nil {152+ t.Error("Expected nil for invalid cinema ID")153+ }154+}155+156+func TestGetFormat(t *testing.T) {157+ data := &models.Data{158+ Formats: []models.Format{159+ {Slug: "2d-digital", Title: "2D Digital"},160+ {Slug: "the-big-show", Title: "The Big Show"},161+ },162+ }163+164+ client := NewClient()165+ format := client.GetFormat(data, "2d-digital")166+167+ if format == nil {168+ t.Fatal("Expected format, got nil")169+ }170+171+ if format.Title != "2D Digital" {172+ t.Errorf("Expected '2D Digital', got '%s'", format.Title)173+ }174+}175+176+func TestGetAgePolicy(t *testing.T) {177+ data := &models.Data{178+ AgePolicies: []models.AgePolicy{179+ {Slug: "rated-pg-13", Name: "Rated PG-13"},180+ {Slug: "rated-r", Name: "Rated R"},181+ },182+ }183+184+ client := NewClient()185+ policy := client.GetAgePolicy(data, "rated-pg-13")186+187+ if policy == nil {188+ t.Fatal("Expected age policy, got nil")189+ }190+191+ if policy.Name != "Rated PG-13" {192+ t.Errorf("Expected 'Rated PG-13', got '%s'", policy.Name)193+ }194+}@@ -0,0 +1,180 @@
1+package models2+3+type APIResponse struct {4+ Data Data `json:"data"`5+}6+7+type Data struct {8+ Presentations []Presentation `json:"presentations"`9+ RelatedPresentations []interface{} `json:"relatedPresentations"`10+ Market []Market `json:"market"`11+ Sessions []Session `json:"sessions"`12+ PresentationAttributes []PresentationAttribute `json:"presentationAttributes"`13+ SessionAttributes []SessionAttribute `json:"sessionAttributes"`14+ Formats []Format `json:"formats"`15+ AgePolicies []AgePolicy `json:"agePolicies"`16+ Queues []interface{} `json:"queues"`17+}18+19+type Presentation struct {20+ Slug string `json:"slug"`21+ LegacySlug *string `json:"legacySlug"`22+ Show Show `json:"show"`23+ Event *Event `json:"event"`24+ EventType interface{} `json:"eventType"`25+ FormatSlugs []string `json:"formatSlugs"`26+ PresentationAttributeSlugs []string `json:"presentationAttributeSlugs"`27+ AssociatedPresentationSlugs []string `json:"associatedPresentationSlugs"`28+ OpeningDateClt *string `json:"openingDateClt"`29+ OpeningDateDisplay *string `json:"openingDateDisplay"`30+ SuperTitle interface{} `json:"superTitle"`31+ IsHidden bool `json:"isHidden"`32+ PrimaryCollectionSlug *string `json:"primaryCollectionSlug"`33+ IsAvailableForPersonalTheater bool `json:"isAvailableForPersonalTheater"`34+}35+36+type Show struct {37+ Slug string `json:"slug"`38+ LegacySlug *string `json:"legacySlug"`39+ Title string `json:"title"`40+ VistaHoCodes []string `json:"vistaHoCodes"`41+ Headline string `json:"headline"`42+ LandscapeHeroImage Image `json:"landscapeHeroImage"`43+ PortraitHeroImage Image `json:"portraitHeroImage"`44+ PosterImages []Image `json:"posterImages"`45+ Certification string `json:"certification"`46+}47+48+type Image struct {49+ URI string `json:"uri"`50+ AltText string `json:"altText"`51+}52+53+type Event struct {54+ Slug string `json:"slug"`55+ Title string `json:"title"`56+ EventTypeSlug string `json:"eventTypeSlug"`57+ EventTypeTitleOverride *string `json:"eventTypeTitleOverride"`58+ Headline string `json:"headline"`59+ Description string `json:"description"`60+}61+62+type Session struct {63+ CinemaId string `json:"cinemaId"`64+ SessionId string `json:"sessionId"`65+ PresentationSlug string `json:"presentationSlug"`66+ LegacySlug string `json:"legacySlug"`67+ Status string `json:"status"`68+ BusinessDateClt string `json:"businessDateClt"`69+ ShowTimeClt string `json:"showTimeClt"`70+ CinemaTimeZoneName string `json:"cinemaTimeZoneName"`71+ ShowTimeUtc string `json:"showTimeUtc"`72+ TicketTypesLoyaltyCount int `json:"ticketTypesLoyaltyCount"`73+ TicketTypesVoucherCount int `json:"ticketTypesVoucherCount"`74+ TicketTypesNormalCount int `json:"ticketTypesNormalCount"`75+ ReservedSeating bool `json:"reservedSeating"`76+ ScreenNumber string `json:"screenNumber"`77+ FormatSlug string `json:"formatSlug"`78+ IsClosedCaptioningAvailable bool `json:"isClosedCaptioningAvailable"`79+ IsDescriptiveAudioAvailable bool `json:"isDescriptiveAudioAvailable"`80+ IsAssistedListeningAvailable bool `json:"isAssistedListeningAvailable"`81+ SessionAttributeSlugs []string `json:"sessionAttributeSlugs"`82+ RecognitionIds []string `json:"recognitionIds"`83+ AgePolicySlug string `json:"agePolicySlug"`84+ IsHidden bool `json:"isHidden"`85+ TicketingExperience string `json:"ticketingExperience"`86+}87+88+type Market struct {89+ ID string `json:"id"`90+ Slug string `json:"slug"`91+ Name string `json:"name"`92+ Status string `json:"status"`93+ SubscriptionProgramStatus string `json:"subscriptionProgramStatus"`94+ IsOpenAccessSeasonPassMarket bool `json:"isOpenAccessSeasonPassMarket"`95+ IsOpenForBusiness bool `json:"isOpenForBusiness"`96+ IsFoodPreorderRequired bool `json:"isFoodPreorderRequired"`97+ FoodPreorderAvailability string `json:"foodPreorderAvailability"`98+ IsMaxSafetyOperation bool `json:"isMaxSafetyOperation"`99+ Cinemas []Cinema `json:"cinemas"`100+ MarketStrings []MarketString `json:"marketStrings"`101+}102+103+type Cinema struct {104+ ID string `json:"id"`105+ LoyaltyCinemaId string `json:"loyaltyCinemaId"`106+ Slug string `json:"slug"`107+ Name string `json:"name"`108+ Status string `json:"status"`109+ TimeZoneName string `json:"timeZoneName"`110+ Latitude float64 `json:"latitude"`111+ Longitude float64 `json:"longitude"`112+ Street1 string `json:"street1"`113+ Street2 *string `json:"street2"`114+ City string `json:"city"`115+ State string `json:"state"`116+ PostalCode string `json:"postalCode"`117+ Phone string `json:"phone"`118+ Email string `json:"email"`119+ HeroImageUrl string `json:"heroImageUrl"`120+ GoogleMapsUrl string `json:"googleMapsUrl"`121+ AppleMapsUrl string `json:"appleMapsUrl"`122+ AttachedBar AttachedBar `json:"attachedBar"`123+ IsClosedCaptioningAvailable bool `json:"isClosedCaptioningAvailable"`124+ IsDescriptiveAudioAvailable bool `json:"isDescriptiveAudioAvailable"`125+ IsAssistedListeningAvailable bool `json:"isAssistedListeningAvailable"`126+ Menus Menus `json:"menus"`127+ FoodPreorderAvailability string `json:"foodPreorderAvailability"`128+ ParkingNotice string `json:"parkingNotice"`129+ PostPurchaseNotice PostPurchaseNotice `json:"postPurchaseNotice"`130+ IsWageNoticeDisplayed bool `json:"isWageNoticeDisplayed"`131+}132+133+type AttachedBar struct {134+ Name string `json:"name"`135+ WebsiteURL string `json:"websiteUrl"`136+}137+138+type Menus struct {139+ TheaterMenuPdf string `json:"theaterMenuPdf"`140+}141+142+type PostPurchaseNotice struct {143+}144+145+type MarketString struct {146+ Key string `json:"key"`147+ Value string `json:"value"`148+}149+150+type PresentationAttribute struct {151+ Slug string `json:"slug"`152+ Name string `json:"name"`153+ Description string `json:"description"`154+ IsUserVisible bool `json:"isUserVisible"`155+ IsUserFilterable bool `json:"isUserFilterable"`156+}157+158+type SessionAttribute struct {159+ Slug string `json:"slug"`160+ Name string `json:"name"`161+ Description string `json:"description"`162+ IsUserVisible bool `json:"isUserVisible"`163+ IsUserFilterable bool `json:"isUserFilterable"`164+}165+166+type Format struct {167+ Slug string `json:"slug"`168+ VistaSlug *string `json:"vistaSlug"`169+ Title string `json:"title"`170+ Headline string `json:"headline"`171+ Description string `json:"description"`172+ TitleTreatmentImage Image `json:"titleTreatmentImage"`173+ HeroImage *Image `json:"heroImage"`174+}175+176+type AgePolicy struct {177+ Slug string `json:"slug"`178+ Name string `json:"name"`179+ Description string `json:"description"`180+}@@ -0,0 +1,266 @@
1+package state2+3+import (4+ "fmt"5+6+ "github.com/charmbracelet/bubbles/list"7+ "github.com/charmbracelet/bubbletea"8+ "github.com/charmbracelet/lipgloss"9+ "github.com/dami/showings/internal/api"10+ "github.com/dami/showings/internal/models"11+ "github.com/dami/showings/internal/views"12+)13+14+type ViewState int15+16+const (17+ ViewLoading ViewState = iota18+ ViewPresentations19+ ViewSessions20+)21+22+type FetchError struct {23+ Err error24+}25+26+type DataLoaded struct {27+ Data models.Data28+ Presentations []models.Presentation29+}30+31+type Model struct {32+ viewState ViewState33+ presentations []models.Presentation34+ sessions []models.Session35+ data *models.Data36+ apiClient *api.Client37+ venue string38+39+ presentationList list.Model40+ spinner int41+ err error42+}43+44+func New(venue string) *Model {45+ return &Model{46+ viewState: ViewLoading,47+ venue: venue,48+ apiClient: api.NewClient(),49+ }50+}51+52+func (m *Model) Init() tea.Cmd {53+ return m.fetchData54+}55+56+func (m *Model) fetchData() tea.Msg {57+ data, err := m.apiClient.FetchSchedule(m.venue)58+ if err != nil {59+ return FetchError{err}60+ }61+62+ presentations := m.apiClient.FilterUpcomingPresentations(&data.Data)63+64+ return DataLoaded{65+ Data: data.Data,66+ Presentations: presentations,67+ }68+}69+70+func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {71+ switch msg := msg.(type) {72+ case FetchError:73+ m.err = msg.Err74+ return m, nil75+76+ case DataLoaded:77+ m.data = &msg.Data78+ m.presentations = msg.Presentations79+ m.viewState = ViewPresentations80+ m.initPresentationList()81+ return m, nil82+83+ case tea.KeyMsg:84+ switch msg.String() {85+ case "esc":86+ if m.viewState == ViewSessions {87+ m.viewState = ViewPresentations88+ }89+ return m, nil90+91+ case "enter":92+ if m.viewState == ViewPresentations {93+ idx := m.presentationList.Index()94+ if idx >= 0 && idx < len(m.presentations) {95+ m.sessions = m.apiClient.GetSessionsForPresentation(m.data, m.presentations[idx].Slug)96+ m.viewState = ViewSessions97+ }98+ }99+ return m, nil100+ }101+ }102+103+ if m.viewState == ViewPresentations {104+ var cmd tea.Cmd105+ m.presentationList, cmd = m.presentationList.Update(msg)106+ return m, cmd107+ }108+109+ return m, nil110+}111+112+func (m *Model) View() string {113+ if m.err != nil {114+ return errorStyle.Render(m.err.Error())115+ }116+117+ switch m.viewState {118+ case ViewLoading:119+ return loadingStyle.Render("Loading schedule...")120+ case ViewPresentations:121+ return m.presentationList.View()122+ case ViewSessions:123+ return m.renderSessionsView()124+ }125+ return ""126+}127+128+func (m *Model) initPresentationList() {129+ items := make([]list.Item, len(m.presentations))130+ for i, p := range m.presentations {131+ items[i] = views.PresentationItem{132+ Presentation: p,133+ SessionCount: len(m.apiClient.GetSessionsForPresentation(m.data, p.Slug)),134+ }135+ }136+137+ m.presentationList = list.New(items, views.PresentationDelegate{}, 0, 0)138+ m.presentationList.Title = "Upcoming Shows"139+ m.presentationList.Styles = listStyles140+}141+142+func (m *Model) renderSessionsView() string {143+ if len(m.sessions) == 0 {144+ return "No sessions available"145+ }146+147+ presentation := m.presentations[m.presentationList.Index()]148+ title := presentation.Show.Title149+150+ header := headerStyle.Render(title)151+ separator := separatorStyle.Render(separatorText)152+ backHint := hintStyle.Render(hintText)153+154+ var content []string155+ content = append(content, header)156+ content = append(content, "")157+ content = append(content, backHint)158+ content = append(content, separator)159+ content = append(content, "")160+161+ type cinemaGroup struct {162+ cinema *models.Cinema163+ sessions []models.Session164+ }165+166+ cinemaMap := make(map[string]*cinemaGroup)167+ for _, s := range m.sessions {168+ cinema := m.apiClient.GetCinema(m.data, s.CinemaId)169+ if cinema == nil {170+ continue171+ }172+ if _, ok := cinemaMap[cinema.ID]; !ok {173+ cinemaMap[cinema.ID] = &cinemaGroup{cinema: cinema}174+ }175+ cinemaMap[cinema.ID].sessions = append(cinemaMap[cinema.ID].sessions, s)176+ }177+178+ for _, cg := range cinemaMap {179+ cinemaHeader := cinemaStyle.Render(cg.cinema.Name)180+ content = append(content, cinemaHeader)181+182+ dateMap := make(map[string][]models.Session)183+ for _, s := range cg.sessions {184+ dateMap[s.BusinessDateClt] = append(dateMap[s.BusinessDateClt], s)185+ }186+187+ for date, sessions := range dateMap {188+ dateHeader := dateStyle.Render(date)189+ content = append(content, " "+dateHeader)190+ for _, s := range sessions {191+ format := m.apiClient.GetFormat(m.data, s.FormatSlug)192+ formatName := ""193+ if format != nil {194+ formatName = format.Title195+ }196+197+ agePolicy := m.apiClient.GetAgePolicy(m.data, s.AgePolicySlug)198+ agePolicyName := ""199+ if agePolicy != nil {200+ agePolicyName = agePolicy.Name201+ }202+203+ sessionLine := sessionStyle.Render(204+ fmt.Sprintf("%s Screen %s %s %s", s.ShowTimeClt[11:16], s.ScreenNumber, formatName, agePolicyName),205+ )206+ content = append(content, " "+sessionLine)207+ }208+ }209+ content = append(content, "")210+ }211+212+ return lipgloss.JoinVertical(lipgloss.Left, content...)213+}214+215+var (216+ loadingStyle lipgloss.Style217+ errorStyle lipgloss.Style218+ headerStyle lipgloss.Style219+ separatorStyle lipgloss.Style220+ separatorText string221+ hintStyle lipgloss.Style222+ hintText string223+ listStyles list.Styles224+ cinemaStyle lipgloss.Style225+ dateStyle lipgloss.Style226+ sessionStyle lipgloss.Style227+)228+229+func init() {230+ loadingStyle = lipgloss.NewStyle().231+ Foreground(lipgloss.Color("86")).232+ Padding(1)233+234+ errorStyle = lipgloss.NewStyle().235+ Foreground(lipgloss.Color("196")).236+ Padding(1)237+238+ headerStyle = lipgloss.NewStyle().239+ Foreground(lipgloss.Color("226")).240+ Bold(true).241+ Padding(0, 0, 1, 0)242+243+ separatorStyle = lipgloss.NewStyle().244+ Foreground(lipgloss.Color("240"))245+246+ separatorText = "────────────────────────────────────────"247+248+ hintStyle = lipgloss.NewStyle().249+ Foreground(lipgloss.Color("245"))250+251+ hintText = "←→ select Enter view Esc back"252+253+ listStyles = list.DefaultStyles()254+255+ cinemaStyle = lipgloss.NewStyle().256+ Foreground(lipgloss.Color("75")).257+ Bold(true).258+ Padding(0, 0, 1, 0)259+260+ dateStyle = lipgloss.NewStyle().261+ Foreground(lipgloss.Color("228")).262+ Bold(true)263+264+ sessionStyle = lipgloss.NewStyle().265+ Foreground(lipgloss.Color("252"))266+}@@ -0,0 +1,95 @@
1+package views2+3+import (4+ "fmt"5+ "io"6+7+ "github.com/charmbracelet/bubbles/list"8+ "github.com/charmbracelet/bubbletea"9+ "github.com/charmbracelet/lipgloss"10+ "github.com/dami/showings/internal/models"11+)12+13+type PresentationItem struct {14+ Presentation models.Presentation15+ SessionCount int16+}17+18+func (i PresentationItem) Title() string {19+ title := i.Presentation.Show.Title20+ if i.Presentation.Show.Certification != "" {21+ title += fmt.Sprintf(" [%s]", i.Presentation.Show.Certification)22+ }23+ return title24+}25+26+func (i PresentationItem) Description() string {27+ attrs := ""28+ if len(i.Presentation.PresentationAttributeSlugs) > 0 {29+ attrs = " • " + joinAttributes(i.Presentation.PresentationAttributeSlugs)30+ }31+ return fmt.Sprintf("%d sessions%s", i.SessionCount, attrs)32+}33+34+func (i PresentationItem) FilterValue() string {35+ return i.Presentation.Show.Title36+}37+38+type PresentationDelegate struct{}39+40+func (d PresentationDelegate) Height() int { return 1 }41+func (d PresentationDelegate) Spacing() int { return 0 }42+func (d PresentationDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }43+func (d PresentationDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {44+ i, ok := item.(PresentationItem)45+ if !ok {46+ return47+ }48+49+ title := i.Title()50+ desc := i.Description()51+52+ ts := titleStyle53+ ds := descStyle54+55+ if index == m.Index() {56+ ts = selectedTitleStyle57+ ds = selectedDescStyle58+ }59+60+ fmt.Fprintf(w, "%s %s", ts.Render(title), ds.Render(desc))61+}62+63+func joinAttributes(slugs []string) string {64+ result := ""65+ for i, s := range slugs {66+ if i > 0 {67+ result += ", "68+ }69+ result += attributeNames[s]70+ }71+ return result72+}73+74+var attributeNames = map[string]string{75+ "first-run": "First Run",76+ "alamo-exclusive": "Alamo Exclusive",77+ "advance-sales": "Advance Sales",78+ "family-friendly": "Family Friendly",79+}80+81+var (82+ titleStyle = lipgloss.NewStyle().83+ Foreground(lipgloss.Color("252")).84+ Bold(false)85+86+ descStyle = lipgloss.NewStyle().87+ Foreground(lipgloss.Color("245"))88+89+ selectedTitleStyle = lipgloss.NewStyle().90+ Foreground(lipgloss.Color("226")).91+ Bold(true)92+93+ selectedDescStyle = lipgloss.NewStyle().94+ Foreground(lipgloss.Color("228"))95+)@@ -0,0 +1,36 @@
1+package main2+3+import (4+ "os"5+6+ "github.com/charmbracelet/bubbletea"7+ "github.com/dami/showings/internal/state"8+ "github.com/spf13/cobra"9+)10+11+var venue string12+13+var rootCmd = &cobra.Command{14+ Use: "showings",15+ Short: "View Alamo Drafthouse showtimes",16+ Run: func(cmd *cobra.Command, args []string) {17+ p := tea.NewProgram(state.New(venue), tea.WithAltScreen())18+ if _, err := p.Run(); err != nil {19+ os.Exit(1)20+ }21+ },22+}23+24+func Execute() error {25+ return rootCmd.Execute()26+}27+28+func init() {29+ rootCmd.Flags().StringVarP(&venue, "venue", "v", "westminster", "Alamo Drafthouse venue slug")30+}31+32+func main() {33+ if err := Execute(); err != nil {34+ os.Exit(1)35+ }36+}@@ -0,0 +1,365 @@
1+# Alamo Drafthouse Schedule/Venue API Response Schema2+3+This document provides a comprehensive breakdown of the JSON response structure from the Alamo Drafthouse theater schedule/venue API.4+5+`https://drafthouse.com/s/mother/v2/schedule/venue/westminster`6+7+---8+9+## 1. Top-Level Structure10+11+The response has a single top-level key `data` containing all information:12+13+```json14+{15+ "data": { ... }16+}17+```18+19+### Top-Level Keys in `data`20+21+| Key | Type | Description |22+|-----|------|-------------|23+| `presentations` | Array | List of movie/show presentations (47 items) |24+| `relatedPresentations` | Array | Related presentations (empty in this response) |25+| `market` | Array | Market/region information (Denver area) |26+| `sessions` | Array | Showtime sessions (209 items) |27+| `presentationAttributes` | Array | Attributes like "first-run", "Alamo Exclusive" (4 items) |28+| `sessionAttributes` | Array | Session types like "Baby Day", "Kid Friendly" (6 items) |29+| `formats` | Array | Screening formats like "2D Digital", "The Big Show" (2 items) |30+| `agePolicies` | Array | Age rating policies (5 items) |31+| `queues` | Array | Queue information (empty in this response) |32+33+---34+35+## 2. Presentations36+37+The `presentations` array contains 47 items, each representing a movie or special event screening.38+39+### Presentation Object Structure40+41+| Field | Type | Description |42+|-------|------|-------------|43+| `slug` | String | Unique identifier for the presentation (e.g., "wuthering-heights-2026") |44+| `legacySlug` | String\|null | Legacy identifier for backward compatibility |45+| `show` | Object | Core movie/show information |46+| `event` | Object\|null | Special event details (if applicable) |47+| `eventType` | String\|null | Type of special event |48+| `formatSlugs` | Array[String] | Screening formats (e.g., ["2d-digital", "the-big-show"]) |49+| `presentationAttributeSlugs` | Array[String] | Presentation tags (e.g., ["first-run", "alamo-exclusive"]) |50+| `associatedPresentationSlugs` | Array[String] | Related presentations (e.g., book club screenings) |51+| `openingDateClt` | String\|null | Opening date in local time |52+| `openingDateDisplay` | String\|null | Display-friendly opening date |53+| `superTitle` | String\|null | Additional title context |54+| `isHidden` | Boolean | Whether the presentation is hidden |55+| `primaryCollectionSlug` | String\|null | Collection grouping |56+| `isAvailableForPersonalTheater` | Boolean | Personal theater availability |57+58+### Nested: `show` Object59+60+The `show` object contains the core movie metadata:61+62+| Field | Type | Description |63+|-------|------|-------------|64+| `slug` | String | Show identifier |65+| `legacySlug` | String\|null | Legacy identifier |66+| `title` | String | Movie title (e.g., "Wuthering Heights (2026)") |67+| `vistaHoCodes` | Array[String] | Vista POS internal codes (e.g., ["A000035278", ...]) |68+| `headline` | String | Marketing headline/synopsis |69+| `landscapeHeroImage` | Object | Wide aspect ratio hero image |70+| `portraitHeroImage` | Object | Portrait aspect ratio hero image |71+| `posterImages` | Array[Object] | Movie poster images |72+| `certification` | String | MPAA rating (e.g., "R", "PG", "PG-13", "NR") |73+74+### Image Object Structure75+76+```json77+{78+ "uri": "https://img-assets.drafthouse.com/images/shows/...",79+ "altText": "WUTHERING HEIGHTS (2026)"80+}81+```82+83+| Field | Type | Description |84+|-------|------|-------------|85+| `uri` | String | Full URL to the image |86+| `altText` | String | Accessibility text |87+88+### Nested: `event` Object (when present)89+90+Special events like Book Club screenings have additional metadata:91+92+| Field | Type | Description |93+|-------|------|-------------|94+| `slug` | String | Event identifier |95+| `title` | String | Event title (may contain HTML like `<show-title>`) |96+| `eventTypeSlug` | String | Type of event (e.g., "book-club-screening") |97+| `eventTypeTitleOverride` | String\|null | Custom event type title |98+| `headline` | String | Event marketing headline |99+| `description` | String | Full event description (HTML) |100+101+---102+103+## 3. Sessions104+105+The `sessions` array contains 209 showtime entries, linking presentations to specific theaters and times.106+107+### Session Object Structure108+109+| Field | Type | Description |110+|-------|------|-------------|111+| `cinemaId` | String | Theater identifier (e.g., "0403") |112+| `sessionId` | String | Unique session ID (e.g., "78490") |113+| `presentationSlug` | String | Reference to presentation |114+| `legacySlug` | String | Legacy presentation reference |115+| `status` | String | Session status ("ONSALE" or "PAST") |116+| `businessDateClt` | String | Business date in local time (ISO 8601: "2026-02-17") |117+| `showTimeClt` | String | Showtime in local time (ISO 8601: "2026-02-17T19:15:00") |118+| `cinemaTimeZoneName` | String | Timezone (e.g., "America/Denver") |119+| `showTimeUtc` | String | Showtime in UTC (ISO 8601: "2026-02-18T02:15:00") |120+| `ticketTypesLoyaltyCount` | Integer | Loyalty ticket availability |121+| `ticketTypesVoucherCount` | Integer | Voucher ticket availability |122+| `ticketTypesNormalCount` | Integer | Standard ticket availability |123+| `reservedSeating` | Boolean | Whether seating is reserved |124+| `screenNumber` | String | Screen/theater number (e.g., "7") |125+| `formatSlug` | String | Format being shown (e.g., "2d-digital") |126+| `isClosedCaptioningAvailable` | Boolean | CC availability |127+| `isDescriptiveAudioAvailable` | Boolean | Audio description availability |128+| `isAssistedListeningAvailable` | Boolean | Assistive listening availability |129+| `sessionAttributeSlugs` | Array[String] | Session tags (e.g., ["BD", "KF"]) |130+| `recognitionIds` | Array | Recognition/tracking IDs |131+| `agePolicySlug` | String | Age policy (e.g., "rated-pg-13") |132+| `isHidden` | Boolean | Whether session is hidden |133+| `ticketingExperience` | String | Ticket flow type (e.g., "DEFAULT") |134+135+---136+137+## 4. Market138+139+The `market` array contains region/theater group information (currently Denver area).140+141+### Market Object Structure142+143+| Field | Type | Description |144+|-------|------|-------------|145+| `id` | String | Market ID (e.g., "0400") |146+| `slug` | String | Market identifier (e.g., "denver") |147+| `name` | String | Display name (e.g., "Denver Area") |148+| `status` | String | Market status ("OPEN") |149+| `subscriptionProgramStatus` | String | Subscription program status ("OPEN") |150+| `isOpenAccessSeasonPassMarket` | Boolean | Season pass availability |151+| `isOpenForBusiness` | Boolean | Whether market is operational |152+| `isFoodPreorderRequired` | Boolean | Food preorder requirement |153+| `foodPreorderAvailability` | String | Food preorder status ("NONE") |154+| `isMaxSafetyOperation` | Boolean | Safety operation flag |155+| `cinemas` | Array[Object] | List of theaters in this market |156+| `marketStrings` | Array[Object] | Localized policy descriptions |157+158+### Nested: `cinema` Object159+160+Each cinema represents a physical theater location:161+162+| Field | Type | Description |163+|-------|------|-------------|164+| `id` | String | Cinema ID (e.g., "0402") |165+| `loyaltyCinemaId` | String | Loyalty program ID |166+| `slug` | String | Cinema identifier (e.g., "sloans-lake") |167+| `name` | String | Theater name (e.g., "Sloans Lake") |168+| `status` | String | Cinema status ("OPEN") |169+| `timeZoneName` | String | Local timezone |170+| `latitude` | Number | GPS latitude |171+| `longitude` | Number | GPS longitude |172+| `street1` | String | Street address |173+| `street2` | String\|null | Address line 2 |174+| `city` | String | City |175+| `state` | String | State code (e.g., "CO") |176+| `postalCode` | String | ZIP code |177+| `phone` | String | Phone number |178+| `email` | String | Contact email |179+| `heroImageUrl` | String | Theater image URL |180+| `googleMapsUrl` | String | Google Maps link |181+| `appleMapsUrl` | String | Apple Maps link |182+| `attachedBar` | Object | Bar information |183+| `isClosedCaptioningAvailable` | Boolean | CC availability |184+| `isDescriptiveAudioAvailable` | Boolean | Audio description availability |185+| `isAssistedListeningAvailable` | Boolean | Assistive listening availability |186+| `menus` | Object | Food menu PDF URLs |187+| `foodPreorderAvailability` | String | Food preorder status |188+| `parkingNotice` | String | Parking information (HTML) |189+| `postPurchaseNotice` | Object | Post-purchase information |190+| `isWageNoticeDisplayed` | Boolean | Wage notice display flag |191+192+### Attached Bar Object193+194+```json195+{196+ "name": "BarFly",197+ "websiteUrl": "https://drafthouse.com/denver/theater-bar/sloans-lake"198+}199+```200+201+### Menus Object202+203+```json204+{205+ "theaterMenuPdf": "https://crafthouse-assets-prod.s3.amazonaws.com/files/venues/..."206+}207+```208+209+### Market Strings Object210+211+Localized accessibility and policy descriptions:212+213+```json214+{215+ "key": "closedCaptioning",216+ "value": "The personal closed captioning device..."217+}218+```219+220+Keys include: `closedCaptioning`, `descriptiveAudio`, `assistedListening`, `latePolicy`221+222+---223+224+## 5. Presentation Attributes225+226+Presentation attributes describe special presentation types.227+228+| Field | Type | Description |229+|-------|------|-------------|230+| `slug` | String | Attribute identifier |231+| `name` | String | Display name |232+| `description` | String | Attribute description |233+| `isUserVisible` | Boolean | Whether shown to users |234+| `isUserFilterable` | Boolean | Whether filterable |235+236+**Values in this response:**237+- `first-run` - New Releases238+- `alamo-exclusive` - Only at the Alamo239+- `advance-sales` - Advance Sales240+- `family-friendly` - Family Friendly241+242+---243+244+## 6. Session Attributes245+246+Session attributes describe special session types (Baby Day, Kid Friendly, etc.).247+248+| Field | Type | Description |249+|-------|------|-------------|250+| `slug` | String | Attribute identifier |251+| `name` | String | Display name |252+| `description` | String | Attribute description |253+| `isUserVisible` | Boolean | Whether shown to users |254+| `isUserFilterable` | Boolean | Whether filterable |255+256+**Values in this response:**257+- `BD` - Baby Day Show258+- `Digital` - Digital259+- `Hidden` - Invite Only260+- `KF` - Kid Friendly261+- `QR Menu 4` - Menu Add Special Screening 4262+- `QR Stay On` - QR Ordering Stays On263+264+---265+266+## 7. Formats267+268+Screening format definitions.269+270+| Field | Type | Description |271+|-------|------|-------------|272+| `slug` | String | Format identifier |273+| `vistaSlug` | String\|null | Vista POS code |274+| `title` | String | Display title |275+| `headline` | String | Marketing headline |276+| `description` | String | Format description (HTML) |277+| `titleTreatmentImage` | Object | Format logo image |278+| `heroImage` | Object\|null | Hero image |279+280+**Values in this response:**281+- `2d-digital` - Standard 2D digital projection282+- `the-big-show` - Premium Large Format (PLF)283+284+---285+286+## 8. Age Policies287+288+Age rating policy definitions.289+290+| Field | Type | Description |291+|-------|------|-------------|292+| `slug` | String | Policy identifier |293+| `name` | String | Display name |294+| `description` | String | Policy rules |295+296+**Values in this response:**297+- `all-ages` - All Ages298+- `rated-pg` - Rated PG299+- `rated-pg-13` - Rated PG-13 (Standard)300+- `rated-pg-adult-focus` - Rated PG with Adult Focus301+- `rated-r` - Rated R302+303+---304+305+## 9. Patterns and Observations306+307+### Data Relationships308+309+1. **Presentation to Session**: Sessions reference presentations via `presentationSlug` field310+2. **Session to Cinema**: Sessions reference theaters via `cinemaId` 311+3. **Session to Format**: Sessions use `formatSlug` referencing the `formats` array312+4. **Session to Age Policy**: Sessions use `agePolicySlug` referencing `agePolicies`313+5. **Presentation to Attributes**: Presentations use `presentationAttributeSlugs` referencing `presentationAttributes`314+315+### Date/Time Handling316+317+- Two timezone-aware time fields are provided:318+ - `showTimeClt` - Local time (client timezone)319+ - `showTimeUtc` - UTC time320+- Sessions span multiple business dates (data shows dates from 2026-02-14 to 2026-03-28)321+- Session `status` indicates availability: "ONSALE" or "PAST"322+323+### Availability Tracking324+325+- Ticket availability is tracked via three fields:326+ - `ticketTypesLoyaltyCount` - Loyalty member tickets327+ - `ticketTypesVoucherCount` - Voucher holder tickets328+ - `ticketTypesNormalCount` - Standard tickets329+330+### Accessibility Features331+332+- Three accessibility options tracked at both cinema and session level:333+ - Closed Captioning334+ - Descriptive Audio335+ - Assisted Listening336+337+### Image Types338+339+- **Landscape Hero**: 1920x1080 - Used for wide displays340+- **Portrait Hero**: 1080x1440 - Used for mobile/vertical341+- **Poster**: 1080x1620 - Standard movie poster342+- **Title Treatment**: SVG format - Format logos343+344+### Certification Values345+346+Movie ratings found in data: `R`, `PG`, `PG-13`, `NR` (Not Rated)347+348+---349+350+## 10. Summary Statistics351+352+| Metric | Count |353+|--------|-------|354+| Total Presentations | 47 |355+| Total Sessions | 209 |356+| Total Cinemas | 3 |357+| Total Formats | 2 |358+| Total Presentation Attributes | 4 |359+| Total Session Attributes | 6 |360+| Total Age Policies | 5 |361+| Total Market Strings | 4 |362+363+---364+365+*Generated from Alamo Drafthouse Schedule/Venue API response*Binary file.