diff options
| -rw-r--r-- | epgpruner/epgpruner.go | 206 | ||||
| -rw-r--r-- | epgtrim/epgtrim.go | 61 | ||||
| -rw-r--r-- | epgtrim/playlist.go | 232 |
3 files changed, 293 insertions, 206 deletions
diff --git a/epgpruner/epgpruner.go b/epgpruner/epgpruner.go deleted file mode 100644 index b2a4f54..0000000 --- a/epgpruner/epgpruner.go +++ /dev/null @@ -1,206 +0,0 @@ -package main - -import ( - "compress/gzip" - "encoding/xml" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "regexp" - "strings" -) - -var ( - docType = `<!DOCTYPE tv SYSTEM "xmltv.dtd">` - - epgRE = regexp.MustCompile(`url-tvg="([a-z0-9-:/.]+)"`) - channelRE = regexp.MustCompile(`tvg-id="([a-z0-9-]+)"`) -) - -func usage() { - fmt.Fprintf(os.Stderr, "usage: epgpruner URL\n") - flag.PrintDefaults() - os.Exit(2) -} - -func main() { - flag.Usage = usage - flag.Parse() - if flag.NArg() != 1 { - usage() - os.Exit(1) - } - playlistURL := flag.Args()[0] - playlist := Playlist{ - URL: playlistURL, - } - err := playlist.Fetch() - if err != nil { - log.Fatal(err) - } - - playlist.Reduce() - - reducedEpg, err := xml.MarshalIndent(playlist.Epg, "", " ") - if err != nil { - log.Fatal(err) - } - fmt.Println( - xml.Header + - docType + "\n" + - string(reducedEpg), - ) -} - -type Playlist struct { - URL string - EpgURL string - Channels map[string]struct{} - Epg Epg -} - -func (p *Playlist) Fetch() error { - compressed := false - body, err := fetch(p.URL, compressed) - if err != nil { - return err - } - err = p.parse(body) - if err != nil { - return err - } - - compressed = true - body, err = fetch(p.EpgURL, compressed) - if err != nil { - return err - } - - err = xml.Unmarshal(body, &p.Epg) - if err != nil { - return err - } - - return nil -} - -func fetch(url string, compressed bool) ([]byte, error) { - resp, err := http.Get(url) - if err != nil { - return make([]byte, 0), err - } - if resp.StatusCode != http.StatusOK { - return make([]byte, 0), - fmt.Errorf("Bad reponse status: %d", resp.StatusCode) - } - reader := resp.Body - defer reader.Close() - if compressed { - reader, err = gzip.NewReader(reader) - if err != nil { - return make([]byte, 0), err - } - } - body, err := io.ReadAll(reader) - if err != nil { - return make([]byte, 0), err - } - return body, nil -} - -func (p *Playlist) parse(body []byte) error { - epgURL := "" - channels := make(map[string]struct{}) - for _, line := range strings.Split(string(body), "\n") { - if strings.HasPrefix(line, "#EXTM3U") { - m := epgRE.FindStringSubmatch(line) - if m == nil { - return fmt.Errorf( - "Couldn't match EPG url from: `%s`", - line, - ) - } - epgURL = m[1] - } else if strings.HasPrefix(line, "#EXTINF") { - m := channelRE.FindStringSubmatch(line) - if m == nil { - return fmt.Errorf( - "Couldn't match channel name from: `%s`", - line, - ) - } - channel := m[1] - channels[channel] = struct{}{} - } - } - - p.EpgURL = epgURL - p.Channels = channels - - return nil -} - -func (p *Playlist) Reduce() { - channels := make([]Channel, 0) - programmes := make([]Programme, 0) - - for _, channel := range p.Epg.Channels { - if _, ok := p.Channels[channel.ID]; !ok { - continue - } - channels = append(channels, channel) - } - - for _, programme := range p.Epg.Programmes { - if _, ok := p.Channels[programme.Channel]; !ok { - continue - } - programmes = append(programmes, programme) - } - - p.Epg.Channels = channels - p.Epg.Programmes = programmes -} - -type Epg struct { - XMLName xml.Name `xml:"tv"` - Info string `xml:"generator-info-name,attr"` - Channels []Channel `xml:"channel"` - Programmes []Programme `xml:"programme"` -} - -type Channel struct { - ID string `xml:"id,attr"` - DisplayName DisplayName `xml:"display-name"` - Icon Icon `xml:"icon"` -} - -type DisplayName struct { - Lang string `xml:"lang,attr"` - Name string `xml:",chardata"` -} - -type Icon struct { - Src string `xml:"src,attr"` -} - -type Programme struct { - Start string `xml:"start,attr"` - Stop string `xml:"stop,attr"` - Channel string `xml:"channel,attr"` - Title Title `xml:"title,omitempty"` - Desc Desc `xml:"desc,omitempty"` -} - -type Title struct { - Lang string `xml:"lang,attr"` - Title string `xml:",chardata"` -} - -type Desc struct { - Lang string `xml:"lang,attr"` - Desc string `xml:",chardata"` -} diff --git a/epgtrim/epgtrim.go b/epgtrim/epgtrim.go new file mode 100644 index 0000000..9fd8d7a --- /dev/null +++ b/epgtrim/epgtrim.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "encoding/xml" + "flag" + "fmt" + "log" + "os" +) + +var ( + docType = `<!DOCTYPE tv SYSTEM "xmltv.dtd">` + output = flag.String( + "output", + "-", + "Write output to `file` (stdout by default)", + ) +) + +func usage() { + fmt.Fprintf(os.Stderr, "usage: epgpruner [-output file] URL\n") + flag.PrintDefaults() + os.Exit(2) +} + +func main() { + log.SetPrefix("epgtrim: ") + log.SetFlags(0) + flag.Usage = usage + flag.Parse() + if flag.NArg() != 1 { + usage() + os.Exit(1) + } + URL := flag.Args()[0] + + playlist, err := NewPlaylist(URL) + if err != nil { + log.Fatal(err) + } + + outfile := os.Stdout + if *output != "-" { + f, err := os.Create(*output) + if err != nil { + log.Fatal(err) + } + outfile = f + defer f.Close() + } + wr := bufio.NewWriter(outfile) + defer wr.Flush() + wr.Write([]byte(xml.Header + docType + "\n")) + e := xml.NewEncoder(wr) + e.Indent("", " ") + err = e.Encode(playlist.Schedule.EPG) + if err != nil { + log.Fatal(err) + } +} diff --git a/epgtrim/playlist.go b/epgtrim/playlist.go new file mode 100644 index 0000000..e179d50 --- /dev/null +++ b/epgtrim/playlist.go @@ -0,0 +1,232 @@ +package main + +import ( + "bufio" + "compress/gzip" + "encoding/xml" + "fmt" + "io" + "net/http" + "regexp" + "strings" +) + +type Playlist struct { + URL string + Channels map[string]struct{} + Schedule Schedule +} + +type Schedule struct { + URL string + EPG EPG +} + +type EPG struct { + XMLName xml.Name `xml:"tv"` + Info string `xml:"generator-info-name,attr"` + Channels []Channel `xml:"channel"` + Programmes []Programme `xml:"programme"` +} + +type Channel struct { + ID string `xml:"id,attr"` + DisplayName DisplayName `xml:"display-name"` + Icon Icon `xml:"icon"` +} + +type DisplayName struct { + Lang string `xml:"lang,attr"` + Name string `xml:",chardata"` +} + +type Icon struct { + Src string `xml:"src,attr"` +} + +type Programme struct { + Start string `xml:"start,attr"` + Stop string `xml:"stop,attr"` + Channel string `xml:"channel,attr"` + Title Title `xml:"title,omitempty"` + Desc Desc `xml:"desc,omitempty"` +} + +type Title struct { + Lang string `xml:"lang,attr"` + Title string `xml:",chardata"` +} + +type Desc struct { + Lang string `xml:"lang,attr"` + Desc string `xml:",chardata"` +} + +func NewPlaylist(URL string) (*Playlist, error) { + pr, err := open(URL) + if err != nil { + return nil, err + } + defer pr.Close() + + scheduleURL, channels, err := parsePlaylist(pr) + if err != nil { + return nil, err + } + + sr, err := open(scheduleURL) + if err != nil { + return nil, err + } + csr, err := gzip.NewReader(sr) + if err != nil { + return nil, err + } + epg, err := parseEPG(csr, channels) + if err != nil { + return nil, err + } + defer sr.Close() + + playlist := Playlist{ + URL: URL, + Channels: channels, + Schedule: Schedule{ + URL: scheduleURL, + EPG: epg, + }, + } + return &playlist, nil +} + +func open(URL string) (io.ReadCloser, error) { + resp, err := http.Get(URL) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, + fmt.Errorf("Bad reponse status: %d", resp.StatusCode) + } + return resp.Body, nil +} + +func parsePlaylist(r io.Reader) (string, map[string]struct{}, error) { + scheduleURL := "" + channels := make(map[string]struct{}) + + br := bufio.NewReader(r) + + for { + line, err := br.ReadString('\n') + if err != nil { + if err != io.EOF { + return "", nil, err + } + break + } + + if strings.HasPrefix(line, "#EXTM3U") { + scheduleURL, err = parseScheduleURL(line) + if err != nil { + return "", nil, err + } + } else if strings.HasPrefix(line, "#EXTINF") { + channelID, err := parseChannelID(line) + if err != nil { + return "", nil, err + } + channels[channelID] = struct{}{} + } + } + + return scheduleURL, channels, nil +} + +var scheduleRE = regexp.MustCompile(`url-tvg="([a-z0-9-:/.]+)"`) + +func parseScheduleURL(line string) (string, error) { + m := scheduleRE.FindStringSubmatch(line) + if m == nil { + return "", fmt.Errorf( + "Couldn't match schedule URL from: `%s`", + line, + ) + } + return m[1], nil +} + +var channelRE = regexp.MustCompile(`tvg-id="([a-z0-9-]+)"`) + +func parseChannelID(line string) (string, error) { + m := channelRE.FindStringSubmatch(line) + if m == nil { + return "", fmt.Errorf( + "Couldn't match channel name from: `%s`", + line, + ) + } + return m[1], nil +} + +func parseEPG(r io.Reader, channels map[string]struct{}) (EPG, error) { + d := xml.NewDecoder(r) + + var epg EPG + + for { + t, err := d.Token() + if err != nil { + if err != io.EOF { + return epg, err + } + break + } + + switch et := t.(type) { + case xml.StartElement: + if et.Name.Local == "tv" { + for _, attr := range et.Attr { + name := attr.Name.Local + if name != "generator-info-name" { + continue + } + epg.Info = attr.Value + break + } + } else if et.Name.Local == "channel" { + // We can check if attribute value is + // in the channels map without decoding + // entire element, but I am too lazy + // for that. Channel have only couple of + // fields, so this should be good enough. + var channel Channel + err := d.DecodeElement(&channel, &et) + if err != nil { + return epg, err + } + if _, ok := channels[channel.ID]; !ok { + continue + } + epg.Channels = append( + epg.Channels, channel, + ) + } else if et.Name.Local == "programme" { + var programme Programme + err := d.DecodeElement(&programme, &et) + if err != nil { + return epg, err + } + channel := programme.Channel + if _, ok := channels[channel]; !ok { + continue + } + epg.Programmes = append( + epg.Programmes, programme, + ) + } + } + } + + return epg, nil +} |