summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--epgpruner/epgpruner.go206
-rw-r--r--epgtrim/epgtrim.go61
-rw-r--r--epgtrim/playlist.go232
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
+}