cair.go (2815B)
1 // Package cair provides a client to version 24.1 of the Cinegy Air 2 // HTTP API as documented at 3 // https://open.cinegy.com/products/air/24.1/cinegy-air-http-api/. 4 // 5 // Typical usage starts with creating a Client, then fetching the 6 // current playlist by calling Playlist on Client. For example, to print 7 // the names of all future items that are 3 minutes or less (e.g. 8 // station breaks): 9 // 10 // client := &Client{http.DefaultClient, "http://air.example.com:5521") 11 // playlist, err := client.Playlist("video") 12 // if err != nil { 13 // // handle error... 14 // } 15 // for _, it := range playlist.Items { 16 // if it.ScheduledAt.Before(time.Now()) { 17 // continue 18 // } 19 // if it.Duration <= 3*time.Minute { 20 // fmt.Println(it.Name) 21 // } 22 // } 23 package cair 24 25 import ( 26 "encoding/xml" 27 "fmt" 28 "net/http" 29 "path" 30 ) 31 32 const DefaultPort = 5521 33 const defaultRoot = "http://127.0.0.1:5521" 34 35 // Client is used to communicate with the Cinegy Air API. 36 type Client struct { 37 *http.Client // http.DefaultClient if nil. 38 // Root is a HTTP URL pointing to the root from the Air API is served. 39 // If empty the client assumes the value "http://127.0.0.1:5521". 40 Root string 41 } 42 43 func (c *Client) get(path string) (*http.Response, error) { 44 if c.Client == nil { 45 c.Client = http.DefaultClient 46 } 47 if c.Root == "" { 48 c.Root = defaultRoot 49 } 50 return c.Get(c.Root + path) 51 } 52 53 // Playlist retrieves the current Playlist for the named device. 54 // Supported names include: 55 // - "video" 56 // - "titler_0" 57 // - "logo" 58 // - "cg_[0-8]" e.g. cg_0, cg_1 ... 59 // - "cg_logo" 60 // - "gfx_[0-8]" 61 // - "audio" 62 // 63 // For all supported names, see Cinegy Air 24.1 HTTP API, chapter 1. 64 func (c *Client) Playlist(device string) (*Playlist, error) { 65 resp, err := c.get(path.Join("/", device, "list")) 66 if err != nil { 67 return nil, err 68 } 69 defer resp.Body.Close() 70 if resp.StatusCode >= http.StatusBadRequest { 71 return nil, fmt.Errorf("non-OK status code from engine: %s", resp.Status) 72 } 73 return ParsePlaylist(resp.Body) 74 } 75 76 func (c *Client) Status() (*Status, error) { 77 resp, err := c.get("/videos/status") 78 if err != nil { 79 return nil, err 80 } 81 defer resp.Body.Close() 82 if resp.StatusCode >= http.StatusBadRequest { 83 return nil, fmt.Errorf("non-OK status code from engine: %s", resp.Status) 84 } 85 var st Status 86 if err := xml.NewDecoder(resp.Body).Decode(&st); err != nil { 87 return nil, fmt.Errorf("decode status: %w", err) 88 } 89 return &st, nil 90 } 91 92 type Status struct { 93 XMLName xml.Name `xml:"Status"` 94 Active struct { 95 ID string `xml:"Id,attr"` 96 } 97 Cued struct { 98 ID string `xml:"Id,attr"` 99 } 100 License struct { 101 State string `xml:",attr"` 102 } 103 Output struct { 104 State string `xml:",attr"` 105 } 106 Client clientStatus 107 } 108 109 type clientStatus struct { 110 XMLName xml.Name `xml:"Client"` 111 Connected string `xml:",attr"` 112 Identity string `xml:",attr"` 113 }