playlist.go (3921B)
1 package cair 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/xml" 7 "fmt" 8 "html" 9 "io" 10 "os" 11 "strconv" 12 "time" 13 ) 14 15 type Playlist struct { 16 XMLName struct{} `xml:"List"` 17 Items []Item `xml:"Item"` 18 } 19 20 type Item struct { 21 ID string `xml:"Id,attr"` 22 Name string `xml:",attr"` 23 Description string `xml:",attr"` 24 ThirdPartyId string `xml:",attr"` 25 SubtitleId string `xml:",attr"` 26 EPGID string `xml:"EpgId,attr"` 27 ProxyProgress string `xml:",attr"` 28 ScheduledAt time.Time `xml:",attr"` 29 Duration time.Duration `xml:",attr"` 30 OutOfNetwork string `xml:",attr"` 31 } 32 33 // EndTime calculates the time i should end by inspecting its start time and duration. 34 func (i *Item) EndTime() (time.Time, error) { 35 return i.ScheduledAt.Add(i.Duration), nil 36 } 37 38 func (i *Item) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error { 39 type alias Item 40 aux := &struct { 41 ScheduledAt string `xml:",attr"` 42 Duration string `xml:",attr"` 43 *alias 44 }{ 45 alias: (*alias)(i), 46 } 47 if err := dec.DecodeElement(aux, &start); err != nil { 48 return err 49 } 50 51 t, err := time.Parse(rfc3339Milli, aux.ScheduledAt) 52 if err != nil { 53 return fmt.Errorf("parse scheduled at: %w", err) 54 } 55 i.ScheduledAt = t 56 dur, err := parseDuration(aux.Duration) 57 if err != nil { 58 return fmt.Errorf("parse duration: %w", err) 59 } 60 i.Duration = dur 61 return nil 62 } 63 64 const rfc3339Milli = "2006-01-02T15:04:05.000Z07:00" 65 66 // parseDuration parses a time.Duration from timecode string. 67 // A timecode represents a duration of time. 68 // A duration of 1 second is represented by the timecode "00:00:01.000". 69 // 12 hours, 34 minutes, 56 seconds and 789 milliseconds is represented by the timecode 70 // "12:34:56.789". 71 func parseDuration(timecode string) (time.Duration, error) { 72 if len(timecode) != 12 { 73 return 0, fmt.Errorf("timecode does not have 12 characters") 74 } 75 76 var duration time.Duration 77 hours, err := strconv.ParseInt(string(timecode[:2]), 10, 0) 78 if err != nil { 79 return 0, fmt.Errorf("parse hours: %v", err) 80 } 81 duration += time.Duration(hours) * time.Hour 82 83 if string(timecode[2]) != ":" { 84 return 0, fmt.Errorf("parse minutes: expected %q, got %q", ":", timecode[3]) 85 } 86 minutes, err := strconv.ParseInt(string(timecode[3:5]), 10, 0) 87 if err != nil { 88 return 0, fmt.Errorf("parse minutes: %v", err) 89 } 90 duration += time.Duration(minutes) * time.Minute 91 92 if string(timecode[5]) != ":" { 93 return 0, fmt.Errorf("parse seconds: expected %q, got %q", ":", timecode[5]) 94 } 95 seconds, err := strconv.ParseInt(string(timecode[6:8]), 10, 0) 96 if err != nil { 97 return 0, fmt.Errorf("parse seconds: %v", err) 98 } 99 duration += time.Duration(seconds) * time.Second 100 101 if string(timecode[8]) != "." { 102 return 0, fmt.Errorf("parse milliseconds: expected %q, got %q", ".", timecode[6]) 103 } 104 milliseconds, err := strconv.ParseInt(string(timecode[9:]), 10, 0) 105 if err != nil { 106 return 0, fmt.Errorf("parse milliseconds: %v", err) 107 } 108 duration += time.Duration(milliseconds) * time.Millisecond 109 110 return duration, nil 111 } 112 113 func playlistFromFile(name string) (*Playlist, error) { 114 f, err := os.Open(name) 115 if err != nil { 116 return nil, err 117 } 118 defer f.Close() 119 return ParsePlaylist(f) 120 } 121 122 // ParsePlaylist parses a XML-encoded Playlist from r. 123 func ParsePlaylist(r io.Reader) (*Playlist, error) { 124 var p Playlist 125 escaped := escapeAmpersand(r) 126 if err := xml.NewDecoder(escaped).Decode(&p); err != nil { 127 return nil, err 128 } 129 return &p, nil 130 } 131 132 // escapeAmpersand returns a new reader which reads from r, escaping all ampersand 133 // characters according to the XML spec. 134 // This is a hack to make Playlists encoded as invalid XML from third party sources valid. 135 func escapeAmpersand(r io.Reader) io.Reader { 136 buf := &bytes.Buffer{} 137 scanner := bufio.NewScanner(r) 138 for scanner.Scan() { 139 buf.Write(bytes.ReplaceAll(scanner.Bytes(), []byte("&"), []byte(html.EscapeString("&")))) 140 } 141 return buf 142 }