├── lineup.go
├── .gitattributes
├── go.mod
├── cronjob
├── .gitignore
├── .idea
└── .gitignore
├── RELEASE_NOTES
├── struct_menu.go
├── go.sum
├── Dockerfile
├── .github
├── workflows
│ ├── development.yaml
│ └── stable.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── sample-config.yaml
├── toolchain.go
├── LICENSE
├── main.go
├── server.go
├── struct_config.go
├── screen.go
├── channels.go
├── struct_xmltv.go
├── struct_cache.go
├── data.go
├── menu.go
├── sd.go
├── xmltv.go
├── configure.go
├── struct_sd.go
├── README.md
└── cache.go
/lineup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module main
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/gorilla/mux v1.8.0
7 | gopkg.in/yaml.v3 v3.0.1
8 | )
9 |
--------------------------------------------------------------------------------
/cronjob:
--------------------------------------------------------------------------------
1 | @reboot /app/guide2go --config /data/livetv/config.yaml
2 | 00 * * * * /app/guide2go --config /data/livetv/config.yaml
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | compiler
2 | build.sh
3 | guide2go
4 | guide2go.sublime-project
5 | guide2go.sublime-workspace
6 | guide2go.code-workspace
7 | dev
8 | *.xml
9 | a.yaml
10 | *.json
11 | *.iml
12 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/RELEASE_NOTES:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 |
4 | ## 26-10-2022
5 | - Added API endpoint to run the EPG grabber
6 | - Server for images
7 | - Added images from local server to the XMLTV file
8 | - Added Livetv/New icon to the title of shows/movies in the XMLTV file
9 |
--------------------------------------------------------------------------------
/struct_menu.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Menu : Menu
4 | type Menu struct {
5 | Entry map[int]Entry
6 | Headline string
7 | Select string
8 | }
9 |
10 | // Entry : Menu entry
11 | type Entry struct {
12 | Key int
13 | Value string
14 |
15 | // Add Lineup
16 | Country string
17 | Postalcode string
18 | ShortName string
19 | Lineup string
20 | }
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
3 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine3.10 as builder
2 |
3 | RUN mkdir /app
4 | COPY *.go /app/
5 | WORKDIR /app
6 | RUN go mod init main
7 | RUN go get
8 | RUN go build -o guide2go
9 |
10 | FROM debian:10-slim
11 |
12 | COPY --from=builder /app/guide2go /usr/local/bin/guide2go
13 | COPY sample-config.yaml /config/sample-config.yaml
14 |
15 | RUN apt-get update && apt-get install ca-certificates -y && apt autoclean
16 |
17 | CMD [ "guide2go", "--config", "/config/sample-config.yaml" ]
--------------------------------------------------------------------------------
/.github/workflows/development.yaml:
--------------------------------------------------------------------------------
1 | name: Development workflow
2 |
3 | on:
4 | push:
5 | branches:
6 | - development
7 |
8 | jobs:
9 | image-build-push-prod:
10 | runs-on: self-hosted
11 | steps:
12 | - name: Build the Docker image
13 | run: |
14 | new_tag=$(date +%s)
15 | docker build . --file Dockerfile --tag chuchodavids/guide2go:"$new_tag" --tag chuchodavids/guide2go:development
16 | docker push chuchodavids/guide2go:"$new_tag"
17 | docker push chuchodavids/guide2go:development
18 |
--------------------------------------------------------------------------------
/.github/workflows/stable.yml:
--------------------------------------------------------------------------------
1 | name: Stable Workflow
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | go-build:
10 | runs-on: self-hosted
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: go build
14 | run: go build -o guide2go
15 | image-build-push-prod:
16 | needs: go-build
17 | runs-on: self-hosted
18 | steps:
19 | - name: Build the Docker image
20 | run: |
21 | new_tag=$(date +%s)
22 | docker build . --file Dockerfile --tag chuchodavids/guide2go:"$new_tag" --tag chuchodavids/guide2go:stable --tag chuchodavids/guide2go:latest
23 | docker push chuchodavids/guide2go:"$new_tag"
24 | docker push chuchodavids/guide2go:stable
25 |
--------------------------------------------------------------------------------
/sample-config.yaml:
--------------------------------------------------------------------------------
1 | Account:
2 | Username:
3 | Password:
4 | Files:
5 | Cache: /config/cache.json
6 | XMLTV: /config/xml.xml
7 | Options:
8 | Poster Aspect: 3x4
9 | Schedule Days: 1
10 | Subtitle into Description: false
11 | Insert credits tag into XML file: false
12 | Local Images Cache: true
13 | Images Path: /data/images
14 | Proxy Images: false
15 | Hostname: localhost:9090
16 | Rating:
17 | Insert rating tag into XML file: true
18 | Maximum rating entries. 0 for all entries: 1
19 | Preferred countries. ISO 3166-1 alpha-3 country code. Leave empty for all systems:
20 | - USA
21 | Use country code as rating system: false
22 | Show download errors from Schedules Direct in the log: true
23 | Station:
24 | - Name: HBO
25 | ID: "19548"
26 | Lineup: USA-IL67050-X
27 | # Keep adding channels as needed
28 |
--------------------------------------------------------------------------------
/toolchain.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "crypto/sha1"
7 | "fmt"
8 | "io"
9 | "strings"
10 | )
11 |
12 | // SHA1 : SHA1
13 | func SHA1(str string) (strSHA1 string) {
14 |
15 | h := sha1.New()
16 | io.WriteString(h, str)
17 | strSHA1 = strings.ToLower(fmt.Sprintf("% x", h.Sum(nil)))
18 | strSHA1 = strings.Replace(strSHA1, " ", "", -1)
19 |
20 | return
21 | }
22 |
23 | // ContainsString : Get string position in slice
24 | func ContainsString(slice []string, e string) int {
25 | for i, a := range slice {
26 | if a == e {
27 | return i
28 | }
29 | }
30 | return -1
31 | }
32 |
33 | func gUnzip(data []byte) (res []byte, err error) {
34 |
35 | b := bytes.NewBuffer(data)
36 |
37 | var r io.Reader
38 | r, err = gzip.NewReader(b)
39 | if err != nil {
40 | return
41 | }
42 |
43 | var resB bytes.Buffer
44 | _, err = resB.ReadFrom(r)
45 | if err != nil {
46 | return
47 | }
48 |
49 | res = resB.Bytes()
50 |
51 | return
52 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 marmei
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "os"
8 | )
9 |
10 | // AppName : App name
11 | const AppName = "guide2go"
12 |
13 | // Version : Version
14 | const Version = "1.2.0"
15 |
16 | // Config : Config file (struct)
17 | var Config config
18 | var Config2 string
19 |
20 | func main() {
21 | log.SetOutput(os.Stdout)
22 | var configure = flag.String("configure", "", "= Create or modify the configuration file. [filename.yaml]")
23 | var config = flag.String("config", "", "= Get data from Schedules Direct with configuration file. [filename.yaml]")
24 |
25 | var h = flag.Bool("h", false, ": Show help")
26 |
27 | flag.Parse()
28 | Config2 = *config
29 | showInfo("G2G", fmt.Sprintf("Version: %s", Version))
30 |
31 | if *h {
32 | fmt.Println()
33 | flag.Usage()
34 | os.Exit(0)
35 | }
36 |
37 | if len(*configure) != 0 {
38 | err := Configure(*configure)
39 | if err != nil {
40 | ShowErr(err)
41 | }
42 | os.Exit(0)
43 | }
44 |
45 | if len(*config) != 0 {
46 | var sd SD
47 | err := sd.Update(*config)
48 | if err != nil {
49 | ShowErr(err)
50 | }
51 | if Config.Options.TVShowImages || Config.Options.ProxyImages {
52 | Server()
53 | os.Exit(0)
54 | }
55 |
56 | }
57 | }
58 |
59 | // ShowErr : Show error on screen
60 | func ShowErr(err error) {
61 | var msg = fmt.Sprintf("[ERROR] %s", err)
62 | log.Println(msg)
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | "strings"
9 |
10 | "github.com/gorilla/mux"
11 | )
12 |
13 | func Server() {
14 | log.SetOutput(os.Stdout)
15 | port := strings.Split(Config.Options.Hostname, ":")
16 | var addr string
17 | serverImagesPath := Config.Options.ImagesPath
18 | fs := http.FileServer(http.Dir(serverImagesPath))
19 | if len(port) == 2 {
20 | addr = ":" + port[1]
21 | } else {
22 | log.Println("No port found, using port 8080")
23 | addr = ":8080"
24 | }
25 |
26 | log.Printf("Listening on: %s", addr)
27 | log.Printf("Using %s folder as image path", serverImagesPath)
28 |
29 | r := mux.NewRouter()
30 |
31 | if Config.Options.ProxyImages {
32 | r.HandleFunc("/images/{id}", proxyImages)
33 | } else if Config.Options.TVShowImages {
34 | r.PathPrefix("/images/").Handler(http.StripPrefix("/images/", fs))
35 | }
36 | r.HandleFunc("/run", run)
37 |
38 | err := http.ListenAndServe(addr, r)
39 | if err != nil {
40 | log.Fatal(err)
41 | }
42 | }
43 |
44 | func proxyImages(w http.ResponseWriter, r *http.Request) {
45 | image := mux.Vars(r)
46 | url := "https://json.schedulesdirect.org/20141201/image/" + image["id"] + "?token=" + Token
47 | a, _ := http.NewRequest("GET", url, nil)
48 | http.Redirect(w, a, url, http.StatusSeeOther)
49 | log.Println("requested image: " + r.RequestURI)
50 | }
51 |
52 | func run(w http.ResponseWriter, r *http.Request) {
53 | var sd SD
54 | go sd.Update(Config2)
55 | fmt.Fprint(w, "Grabbing EPG")
56 | }
57 |
--------------------------------------------------------------------------------
/struct_config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type config struct {
4 | File string `yaml:"-"`
5 | ChannelIDs []string `yaml:"-"`
6 |
7 | Account struct {
8 | Username string `yaml:"Username" json:"username"`
9 | Password string `yaml:"Password" json:"password"`
10 | } `yaml:"Account"`
11 |
12 | Files struct {
13 | Cache string `yaml:"Cache"`
14 | XMLTV string `yaml:"XMLTV"`
15 | } `yaml:"Files"`
16 |
17 | Options struct {
18 | PosterAspect string `yaml:"Poster Aspect"`
19 | Schedule int `yaml:"Schedule Days"`
20 | SubtitleIntoDescription bool `yaml:"Subtitle into Description"`
21 | Credits bool `yaml:"Insert credits tag into XML file"`
22 | TVShowImages bool `yaml:"Local Images Cache"`
23 | ImagesPath string `yaml:"Images Path"`
24 | ProxyImages bool `yaml:"Proxy Images"`
25 | Hostname string `yaml:"Hostname"`
26 |
27 | Rating struct {
28 | Guidelines bool `yaml:"Insert rating tag into XML file"`
29 | MaxEntries int `yaml:"Maximum rating entries. 0 for all entries"`
30 | Countries []string `yaml:"Preferred countries. ISO 3166-1 alpha-3 country code. Leave empty for all systems"`
31 | CountryCodeAsSystem bool `yaml:"Use country code as rating system"`
32 | } `yaml:"Rating"`
33 |
34 | SDDownloadErrors bool `yaml:"Show download errors from Schedules Direct in the log"`
35 | } `yaml:"Options"`
36 |
37 | Station []channel `yaml:"Station"`
38 | }
39 |
40 | type channel struct {
41 | Name string `yaml:"Name" json:"-" xml:"-"`
42 | DisplayName []DisplayName `yaml:"-" json:"-" xml:"display-name"`
43 | ID string `yaml:"ID" json:"stationID" xml:"id,attr"`
44 | Lineup string `yaml:"Lineup" json:"-" xml:"-"`
45 | Date []string `yaml:"-" json:"date"`
46 | Icon Icon `yaml:"-" json:"-" xml:"icon"`
47 | }
48 |
--------------------------------------------------------------------------------
/screen.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "os"
8 | "sort"
9 | "strconv"
10 | )
11 |
12 | func getMsg(code int) (msg string) {
13 |
14 | switch code {
15 |
16 | // Menu entries
17 | case 0000:
18 | msg = "Configuration"
19 | case 0001:
20 | msg = "Select Entry"
21 | case 0010:
22 | msg = "Exit"
23 | case 0011:
24 | msg = "Schedules Direct Account"
25 | case 0012:
26 | msg = "Add Lineup"
27 | case 0013:
28 | msg = "Remove Lineup"
29 | case 0014:
30 | msg = "Manage Channels"
31 | case 0015:
32 | msg = "Exit"
33 | case 0016:
34 | msg = "Create XMLTV File"
35 |
36 | case 0100:
37 | msg = "Username"
38 | case 0101:
39 | msg = "Password"
40 |
41 | case 0200:
42 | msg = "Cancel"
43 | case 0201:
44 | msg = "Select Country"
45 | case 0202:
46 | msg = "Postal Code"
47 | case 0203:
48 | msg = "Select Provider"
49 | case 0204:
50 | msg = "Select Lineup"
51 |
52 | case 0300:
53 | msg = "Update Config File"
54 | case 0301:
55 | msg = "Remove Cache File"
56 |
57 | case 401:
58 | msg = "Download images"
59 |
60 | case 402:
61 | msg = "Dowloaded Images Path"
62 |
63 | case 403:
64 | msg = "Local Images Cache"
65 | }
66 |
67 | return
68 | }
69 |
70 | // Show : Show menu on screen
71 | func (m *Menu) Show() (selection int) {
72 |
73 | log.SetOutput(os.Stdout)
74 | if len(m.Entry) == 0 {
75 | return
76 | }
77 |
78 | fmt.Println()
79 | fmt.Println(m.Headline)
80 |
81 | for i := 0; i < len(m.Headline); i++ {
82 | fmt.Print("-")
83 | }
84 |
85 | fmt.Println()
86 |
87 | for {
88 |
89 | var input string
90 | var keys []int
91 |
92 | for _, entry := range m.Entry {
93 |
94 | keys = append(keys, entry.Key)
95 |
96 | }
97 |
98 | sort.Ints(keys)
99 |
100 | if keys[0] == 0 {
101 | keys = keys[1:]
102 | keys = append(keys, 0)
103 | }
104 |
105 | for _, key := range keys {
106 |
107 | var entry = m.Entry[key]
108 |
109 | switch len(fmt.Sprintf("%d", entry.Key)) {
110 |
111 | case 1:
112 | fmt.Print(fmt.Sprintf(" %d. ", entry.Key))
113 | case 2:
114 | fmt.Print(fmt.Sprintf("%d. ", entry.Key))
115 |
116 | }
117 |
118 | fmt.Println(entry.Value)
119 |
120 | }
121 |
122 | fmt.Print(fmt.Sprintf("%s: ", m.Select))
123 | fmt.Scanln(&input)
124 |
125 | selection, err := strconv.Atoi(input)
126 | if err == nil {
127 |
128 | for _, entry := range m.Entry {
129 |
130 | if selection == entry.Key {
131 | return selection
132 | }
133 |
134 | }
135 |
136 | }
137 |
138 | err = errors.New("Invalid Input")
139 | ShowErr(err)
140 | fmt.Println()
141 |
142 | }
143 |
144 | return
145 | }
146 |
147 | // ShowInfo : Show info on screen
148 | func showInfo(key, msg string) {
149 | log.SetOutput(os.Stdout)
150 | switch len(key) {
151 |
152 | case 1:
153 | msg = fmt.Sprintf("[%s ] %s", key, msg)
154 | case 2:
155 | msg = fmt.Sprintf("[%s ] %s", key, msg)
156 | case 3:
157 | msg = fmt.Sprintf("[%s ] %s", key, msg)
158 | case 4:
159 | msg = fmt.Sprintf("[%s ] %s", key, msg)
160 | case 5:
161 | msg = fmt.Sprintf("[%s] %s", key, msg)
162 |
163 | }
164 |
165 | log.Println(msg)
166 | }
167 |
--------------------------------------------------------------------------------
/channels.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 | )
8 |
9 | func (e *Entry) manageChannels(sd *SD) (err error) {
10 |
11 | defer func() {
12 | Config.Save()
13 | Cache.Save()
14 | }()
15 |
16 | var index, selection int
17 |
18 | var menu Menu
19 | var entry Entry
20 |
21 | err = Cache.Open()
22 | if err != nil {
23 | ShowErr(err)
24 | return
25 | }
26 |
27 | Cache.Init()
28 |
29 | menu.Entry = make(map[int]Entry)
30 |
31 | menu.Select = getMsg(0204)
32 | menu.Headline = e.Value
33 |
34 | // Cancel
35 | entry.Key = index
36 | entry.Value = getMsg(0200)
37 | menu.Entry[index] = entry
38 |
39 | var ch channel
40 |
41 | for _, lineup := range sd.Resp.Status.Lineups {
42 |
43 | index++
44 | entry.Key = index
45 | entry.Value = fmt.Sprintf("%s [%s]", lineup.Name, lineup.Lineup)
46 | entry.Lineup = lineup.Lineup
47 |
48 | menu.Entry[index] = entry
49 |
50 | }
51 |
52 | selection = menu.Show()
53 |
54 | switch selection {
55 |
56 | case 0:
57 | return
58 |
59 | default:
60 | entry = menu.Entry[selection]
61 | ch.Lineup = entry.Lineup
62 |
63 | }
64 |
65 | sd.Req.Parameter = fmt.Sprintf("/%s", entry.Lineup)
66 | sd.Req.Type = "GET"
67 |
68 | err = sd.Lineups()
69 |
70 | entry.headline()
71 | var channelNames []string
72 | var existing string
73 | var addAll, removeAll bool
74 |
75 | for _, station := range sd.Resp.Lineup.Stations {
76 | channelNames = append(channelNames, station.Name)
77 | }
78 |
79 | sort.Strings(channelNames)
80 |
81 | Config.GetChannels()
82 |
83 | for _, cName := range channelNames {
84 |
85 | for _, station := range sd.Resp.Lineup.Stations {
86 |
87 | if cName == station.Name {
88 |
89 | var input string
90 |
91 | ch.Name = fmt.Sprintf("%s", station.Name)
92 | ch.ID = station.StationID
93 |
94 | if ContainsString(Config.ChannelIDs, station.StationID) != -1 {
95 | existing = "+"
96 | } else {
97 | existing = "-"
98 | }
99 |
100 | if !addAll && !removeAll {
101 |
102 | fmt.Println(fmt.Sprintf("[%s] %s [%s] %v", existing, station.Name, station.StationID, station.BroadcastLanguage))
103 |
104 | fmt.Print("(Y) Add Channel, (N) Skip / Remove Channel, (ALL) Add all other Channels, (NONE) Remove all other channels, (SKIP) Skip all Channels: ")
105 | fmt.Scanln(&input)
106 |
107 | switch strings.ToLower(input) {
108 |
109 | case "y":
110 | if existing == "-" {
111 | Config.AddChannel(&ch)
112 | }
113 |
114 | case "n":
115 | if existing == "+" {
116 | Config.RemoveChannel(&ch)
117 | }
118 |
119 | case "all":
120 | Config.AddChannel(&ch)
121 | addAll = true
122 |
123 | case "none":
124 | Config.RemoveChannel(&ch)
125 | removeAll = true
126 |
127 | case "skip":
128 | return
129 |
130 | }
131 |
132 | } else {
133 |
134 | if removeAll {
135 | if existing == "+" {
136 | Config.RemoveChannel(&ch)
137 | }
138 | }
139 |
140 | if addAll {
141 | if existing == "-" {
142 | Config.AddChannel(&ch)
143 | }
144 | }
145 |
146 | }
147 |
148 | }
149 |
150 | }
151 |
152 | }
153 |
154 | return
155 | }
156 |
157 | func (c *config) AddChannel(ch *channel) {
158 |
159 | c.Station = append(c.Station, *ch)
160 |
161 | }
162 |
163 | func (c *config) RemoveChannel(ch *channel) {
164 |
165 | var tmp []channel
166 |
167 | for _, old := range c.Station {
168 |
169 | if old.ID != ch.ID {
170 | tmp = append(tmp, old)
171 | }
172 |
173 | }
174 |
175 | c.Station = tmp
176 | }
177 |
178 | func (c *config) GetChannels() {
179 |
180 | c.ChannelIDs = []string{}
181 |
182 | for _, channel := range c.Station {
183 | c.ChannelIDs = append(c.ChannelIDs, channel.ID)
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/struct_xmltv.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "encoding/xml"
4 |
5 | // Programme : Programme
6 | type Programme struct {
7 | XMLName xml.Name `xml:"programme"`
8 | Channel string `xml:"channel,attr"`
9 | Start string `xml:"start,attr"`
10 | Stop string `xml:"stop,attr"`
11 |
12 | Title []Title `xml:"title"`
13 | SubTitle SubTitle `xml:"sub-title"`
14 |
15 | Desc []Desc `xml:"desc"`
16 |
17 | // Credits
18 | Credits Credits `xml:"credits,omitempty"`
19 |
20 | Categorys []Category `xml:"category,omitempty"`
21 | Language string `xml:"language,omitempty"`
22 | EpisodeNums []EpisodeNum `xml:"episode-num,omitempty"`
23 |
24 | //Icon
25 | Icon []Icon `xml:"icon"`
26 | Video Video `xml:"video"`
27 | Audio Audio `xml:"audio"`
28 |
29 | Rating []Rating `xml:"rating,omitempty"`
30 |
31 | PreviouslyShown *PreviouslyShown `xml:"previously-shown,omitempty"`
32 | New *New `xml:"new"`
33 | Live *Live `xml:"live"`
34 | }
35 |
36 | // ChannelXML : Channel
37 | type ChannelXML struct {
38 | ID string `xml:"id,attr"`
39 | DisplayName []DisplayName `xml:"display-name"`
40 | Icon Icon `xml:"icon"`
41 | }
42 |
43 | // DisplayName : Channel name
44 | type DisplayName struct {
45 | Value string `xml:",chardata"`
46 | }
47 |
48 | // Icon : Channel icon
49 | type Icon struct {
50 | Src string `xml:"src,attr"`
51 | Height int `xml:"height,attr"`
52 | Width int `xml:"width,attr"`
53 | }
54 |
55 | // Title : Title
56 | type Title struct {
57 | Value string `xml:",chardata"`
58 | Lang string `xml:"lang,attr"`
59 | }
60 |
61 | // SubTitle : Sub Title
62 | type SubTitle struct {
63 | Value string `xml:",chardata"`
64 | Lang string `xml:"lang,attr"`
65 | }
66 |
67 | // Desc : Description
68 | type Desc struct {
69 | Value string `xml:",chardata"`
70 | Lang string `xml:"lang,attr"`
71 | }
72 |
73 | // Credits : Credits
74 | type Credits struct {
75 | Director []Director `xml:"director,omitempty"`
76 | Actor []Actor `xml:"actor,omitempty"`
77 | Producer []Producer `xml:"producer,omitempty"`
78 | Presenter []Presenter `xml:"presenter,omitempty"`
79 | Writer []Writer `xml:"writer,omitempty"`
80 | }
81 |
82 | type Director struct {
83 | Value string `xml:",chardata"`
84 | }
85 |
86 | type Producer struct {
87 | Value string `xml:",chardata"`
88 | }
89 |
90 | type Presenter struct {
91 | Value string `xml:",chardata"`
92 | }
93 |
94 | type Actor struct {
95 | Value string `xml:",chardata"`
96 | Role string `xml:"role,attr,omitempty"`
97 | }
98 |
99 | type Writer struct {
100 | Value string `xml:",chardata"`
101 | }
102 |
103 | type Category struct {
104 | Value string `xml:",chardata"`
105 | Lang string `xml:"lang,attr"`
106 | }
107 |
108 | type EpisodeNum struct {
109 | Value string `xml:",chardata"`
110 | System string `xml:"system,attr"`
111 | }
112 |
113 | type ProgramIcon struct {
114 | Src string `xml:"src,attr"`
115 | Height int64 `xml:"height,attr"`
116 | Width int64 `xml:"width,attr"`
117 | }
118 |
119 | type Rating struct {
120 | System string `xml:"system,attr"`
121 | Value string `xml:"value"`
122 | Icon []Icon `xml:"icon",omitempty`
123 | }
124 |
125 | type Video struct {
126 | Present string `xml:"present,omitempty"`
127 | Colour string `xml:"colour,omitempty"`
128 | Aspect string `xml:"aspect,omitempty"`
129 | Quality string `xml:"quality,omitempty"`
130 | }
131 |
132 | type Audio struct {
133 | Stereo string `xml:"stereo,omitempty"`
134 | }
135 |
136 | type PreviouslyShown struct {
137 | Start string `xml:"start,attr,omitempty"`
138 | }
139 |
140 | type New struct {
141 | Value string `xml:",chardata"`
142 | }
143 |
144 | type Live struct {
145 | Value string `xml:",chardata"`
146 | }
147 |
--------------------------------------------------------------------------------
/struct_cache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type cache struct {
9 | Channel map[string]G2GCache `json:"Channel"`
10 | Program map[string]G2GCache `json:"Program"`
11 | Metadata map[string]G2GCache `json:"Metadata"`
12 | Schedule map[string][]G2GCache `json:"Schedule"`
13 |
14 | sync.RWMutex `json:"-"`
15 | }
16 |
17 | // G2GCache : Cache data
18 | type G2GCache struct {
19 |
20 | // Global
21 | Md5 string `json:"md5,omitempty"`
22 | ProgramID string `json:"programID,omitempty"`
23 |
24 | // Channel
25 | StationID string `json:"stationID,omitempty"`
26 | Name string `json:"name,omitempty"`
27 | Callsign string `json:"callsign,omitempty"`
28 | Affiliate string `json:"affiliate,omitempty"`
29 | BroadcastLanguage []string `json:"broadcastLanguage"`
30 | StationLogo []struct {
31 | URL string `json:"URL"`
32 | Height int `json:"height"`
33 | Width int `json:"width"`
34 | Md5 string `json:"md5"`
35 | Source string `json:"source"`
36 | } `json:"stationLogo,omitempty"`
37 | Logo struct {
38 | URL string `json:"URL"`
39 | Height int `json:"height"`
40 | Width int `json:"width"`
41 | Md5 string `json:"md5"`
42 | } `json:"logo,omitempty"`
43 |
44 | // Schedule
45 | AirDateTime time.Time `json:"airDateTime,omitempty"`
46 | AudioProperties []string `json:"audioProperties,omitempty"`
47 | Duration int `json:"duration,omitempty"`
48 | LiveTapeDelay string `json:"liveTapeDelay,omitempty"`
49 | New bool `json:"new,omitempty"`
50 | Ratings []struct {
51 | Body string `json:"body"`
52 | Code string `json:"code"`
53 | } `json:"ratings,omitempty"`
54 | VideoProperties []string `json:"videoProperties,omitempty"`
55 |
56 | // Program
57 | Cast []struct {
58 | BillingOrder string `json:"billingOrder"`
59 | CharacterName string `json:"characterName"`
60 | Name string `json:"name"`
61 | NameID string `json:"nameId"`
62 | PersonID string `json:"personId"`
63 | Role string `json:"role"`
64 | } `json:"cast"`
65 | Crew []struct {
66 | BillingOrder string `json:"billingOrder"`
67 | Name string `json:"name"`
68 | NameID string `json:"nameId"`
69 | PersonID string `json:"personId"`
70 | Role string `json:"role"`
71 | } `json:"crew"`
72 | ContentRating []struct {
73 | Body string `json:"body"`
74 | Code string `json:"code"`
75 | Country string `json:"country"`
76 | } `json:"contentRating"`
77 | Descriptions struct {
78 | Description1000 []struct {
79 | Description string `json:"description"`
80 | DescriptionLanguage string `json:"descriptionLanguage"`
81 | } `json:"description1000"`
82 | Description100 []struct {
83 | DescriptionLanguage string `json:"descriptionLanguage"`
84 | Description string `json:"description"`
85 | } `json:"description100"`
86 | } `json:"descriptions"`
87 |
88 | EpisodeTitle150 string `json:"episodeTitle150,omitempty"`
89 | Genres []string `json:"genres,omitempty"`
90 | HasEpisodeArtwork bool `json:"hasEpisodeArtwork,omitempty"`
91 | HasImageArtwork bool `json:"hasImageArtwork,omitempty"`
92 | HasSeriesArtwork bool `json:"hasSeriesArtwork,omitempty"`
93 |
94 | Metadata []struct {
95 | Gracenote struct {
96 | Episode int `json:"episode"`
97 | Season int `json:"season"`
98 | } `json:"Gracenote"`
99 | } `json:"metadata",omitempty`
100 |
101 | OriginalAirDate string `json:"originalAirDate,omitempty"`
102 | ResourceID string `json:"resourceID,omitempty"`
103 | ShowType string `json:"showType,omitempty"`
104 | Titles []struct {
105 | Title120 string `json:"title120"`
106 | } `json:"titles"`
107 |
108 | // Metadata
109 | Data []Data `json:"data,omitempty"`
110 | }
111 |
112 | // SDSchedule : Schedules Direct schedule data
113 | type SDSchedule struct {
114 |
115 | // Schedule
116 | Programs []struct {
117 | AirDateTime time.Time `json:"airDateTime"`
118 | AudioProperties []string `json:"audioProperties"`
119 | Duration int `json:"duration"`
120 | LiveTapeDelay string `json:"liveTapeDelay"`
121 | New bool `json:"new"`
122 | Md5 string `json:"md5"`
123 | ProgramID string `json:"programID"`
124 | Ratings []struct {
125 | Body string `json:"body"`
126 | Code string `json:"code"`
127 | } `json:"ratings"`
128 | VideoProperties []string `json:"videoProperties"`
129 | } `json:"programs"`
130 | StationID string `json:"stationID"`
131 | }
132 |
--------------------------------------------------------------------------------
/data.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "path/filepath"
8 | "runtime"
9 | "strings"
10 | "sync"
11 | "time"
12 | )
13 |
14 | // Update : Update data from Schedules Direct and create the XMLTV file
15 | func (sd *SD) Update(filename string) (err error) {
16 |
17 | Config.File = strings.TrimSuffix(filename, filepath.Ext(filename))
18 |
19 | _, err = ioutil.ReadFile(fmt.Sprintf("%s.yaml", Config.File))
20 |
21 | if err != nil {
22 | return
23 | }
24 |
25 | err = Config.Open()
26 | if err != nil {
27 | return
28 | }
29 |
30 | err = sd.Init()
31 | if err != nil {
32 | return
33 | }
34 |
35 | if len(sd.Token) == 0 {
36 |
37 | err = sd.Login()
38 | if err != nil {
39 | return
40 | }
41 |
42 | }
43 |
44 | sd.GetData()
45 |
46 | runtime.GC()
47 |
48 | err = CreateXMLTV(filename)
49 | if err != nil {
50 | ShowErr(err)
51 | return
52 | }
53 |
54 | Cache.CleanUp()
55 |
56 | runtime.GC()
57 |
58 | return
59 | }
60 |
61 | // GetData : Get data from Schedules Direct
62 | func (sd *SD) GetData() {
63 |
64 | var err error
65 | var wg sync.WaitGroup
66 | var count = 0
67 |
68 | err = Cache.Open()
69 | if err != nil {
70 | ShowErr(err)
71 | return
72 | }
73 | Cache.Init()
74 |
75 | // Channel list
76 | sd.Status()
77 | Cache.Channel = make(map[string]G2GCache)
78 |
79 | var lineup []string
80 |
81 | for _, l := range sd.Resp.Status.Lineups {
82 | lineup = append(lineup, l.Lineup)
83 | }
84 |
85 | for _, id := range lineup {
86 |
87 | sd.Req.Parameter = fmt.Sprintf("/%s", id)
88 | sd.Req.Type = "GET"
89 |
90 | sd.Lineups()
91 |
92 | Cache.AddStations(&sd.Resp.Body, id)
93 |
94 | }
95 |
96 | // Schedule
97 | showInfo("G2G", fmt.Sprintf("Download Schedule: %d Day(s)", Config.Options.Schedule))
98 |
99 | var limit = 5000
100 |
101 | var days = make([]string, 0)
102 | var channels = make([]interface{}, 0)
103 |
104 | for i := 0; i < Config.Options.Schedule; i++ {
105 | var nextDay = time.Now().Add(time.Hour * time.Duration(24*i))
106 | days = append(days, nextDay.Format("2006-01-02"))
107 | }
108 |
109 | for i, channel := range Config.Station {
110 |
111 | count++
112 |
113 | channel.Date = days
114 | channels = append(channels, channel)
115 |
116 | if count == limit || i == len(Config.Station)-1 {
117 |
118 | sd.Req.Data, err = json.Marshal(channels)
119 | if err != nil {
120 | ShowErr(err)
121 | return
122 | }
123 |
124 | sd.Schedule()
125 |
126 | wg.Add(1)
127 | go func() {
128 |
129 | Cache.AddSchedule(&sd.Resp.Body)
130 |
131 | wg.Done()
132 |
133 | }()
134 |
135 | count = 0
136 |
137 | }
138 |
139 | }
140 |
141 | wg.Wait()
142 |
143 | // Program and Metadata
144 | count = 0
145 | sd.Req.Data = []byte{}
146 |
147 | var types = []string{"programs", "metadata"}
148 | var programIds = Cache.GetRequiredProgramIDs()
149 | var allIDs = Cache.GetAllProgramIDs()
150 | var programs = make([]interface{}, 0)
151 |
152 | showInfo("G2G", fmt.Sprintf("Download Program Informations: New: %d / Cached: %d", len(programIds), len(allIDs)-len(programIds)))
153 |
154 | for _, t := range types {
155 |
156 | switch t {
157 | case "metadata":
158 | sd.Req.URL = fmt.Sprintf("%smetadata/programs", sd.BaseURL)
159 | sd.Req.Call = "metadata"
160 | programIds = Cache.GetRequiredMetaIDs()
161 | limit = 500
162 | showInfo("G2G", fmt.Sprintf("Download missing Metadata: %d ", len(programIds)))
163 |
164 | case "programs":
165 |
166 | sd.Req.URL = fmt.Sprintf("%sprograms", sd.BaseURL)
167 | sd.Req.Call = "programs"
168 | limit = 5000
169 |
170 | }
171 |
172 | for i, p := range programIds {
173 |
174 | count++
175 |
176 | programs = append(programs, p)
177 |
178 | if count == limit || i == len(programIds)-1 {
179 |
180 | sd.Req.Data, err = json.Marshal(programs)
181 | if err != nil {
182 | ShowErr(err)
183 | return
184 | }
185 |
186 | err := sd.Program()
187 | if err != nil {
188 | ShowErr(err)
189 | }
190 |
191 | wg.Add(1)
192 |
193 | switch t {
194 | case "metadata":
195 | go Cache.AddMetadata(&sd.Resp.Body, &wg)
196 |
197 | case "programs":
198 | go Cache.AddProgram(&sd.Resp.Body, &wg)
199 |
200 | }
201 |
202 | count = 0
203 | programs = make([]interface{}, 0)
204 | wg.Wait()
205 |
206 | }
207 |
208 | }
209 |
210 | }
211 |
212 | err = Cache.Save()
213 | if err != nil {
214 | ShowErr(err)
215 | return
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/menu.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func (e *Entry) headline() {
8 |
9 | fmt.Println()
10 | fmt.Println(e.Value)
11 |
12 | for i := 0; i < len(e.Value); i++ {
13 | fmt.Print("-")
14 | }
15 | fmt.Println()
16 |
17 | }
18 |
19 | func (e *Entry) account() {
20 |
21 | var username, password string
22 |
23 | e.headline()
24 |
25 | fmt.Print(fmt.Sprintf("%s: ", getMsg(0100)))
26 | fmt.Scanln(&username)
27 |
28 | fmt.Print(fmt.Sprintf("%s: ", getMsg(0101)))
29 | fmt.Scanln(&password)
30 |
31 | Config.Account.Username = username
32 | Config.Account.Password = SHA1(password)
33 |
34 | Config.Save()
35 |
36 | return
37 | }
38 |
39 | func (e *Entry) addLineup(sd *SD) (err error) {
40 |
41 | var index, selection int
42 | var postalcode string
43 | var menu Menu
44 | var entry Entry
45 |
46 | menu.Entry = make(map[int]Entry)
47 | menu.Select = getMsg(0201)
48 | menu.Headline = e.Value
49 |
50 | err = sd.Countries()
51 | if err != nil {
52 | return
53 | }
54 |
55 | // Cancel
56 | entry.Key = index
57 | entry.Value = getMsg(0200)
58 | menu.Entry[index] = entry
59 |
60 | for _, lineup := range sd.Resp.Countries.NorthAmerica {
61 |
62 | index++
63 | entry.Key = index
64 | entry.Value = fmt.Sprintf("%s [%s]", lineup.FullName, lineup.PostalCodeExample)
65 | entry.Country = lineup.FullName
66 | entry.Postalcode = lineup.PostalCode
67 | entry.ShortName = lineup.ShortName
68 | menu.Entry[index] = entry
69 |
70 | }
71 |
72 | for _, lineup := range sd.Resp.Countries.Europe {
73 |
74 | index++
75 | entry.Key = index
76 | entry.Value = fmt.Sprintf("%s [%s]", lineup.FullName, lineup.PostalCodeExample)
77 | entry.Country = lineup.FullName
78 | entry.Postalcode = lineup.PostalCode
79 | entry.ShortName = lineup.ShortName
80 | menu.Entry[index] = entry
81 |
82 | }
83 |
84 | for _, lineup := range sd.Resp.Countries.LatinAmerica {
85 |
86 | index++
87 | entry.Key = index
88 | entry.Value = fmt.Sprintf("%s [%s]", lineup.FullName, lineup.PostalCodeExample)
89 | entry.Country = lineup.FullName
90 | entry.Postalcode = lineup.PostalCode
91 | entry.ShortName = lineup.ShortName
92 | menu.Entry[index] = entry
93 |
94 | }
95 |
96 | for _, lineup := range sd.Resp.Countries.Caribbean {
97 |
98 | index++
99 | entry.Key = index
100 | entry.Value = fmt.Sprintf("%s [%s]", lineup.FullName, lineup.PostalCodeExample)
101 | entry.Country = lineup.FullName
102 | entry.Postalcode = lineup.PostalCode
103 | entry.ShortName = lineup.ShortName
104 | menu.Entry[index] = entry
105 |
106 | }
107 |
108 | selection = menu.Show()
109 |
110 | switch selection {
111 |
112 | case 0:
113 | return
114 | default:
115 | entry = menu.Entry[selection]
116 |
117 | }
118 |
119 | fmt.Println(entry.Value)
120 |
121 | for {
122 |
123 | fmt.Print(fmt.Sprintf("%s: ", getMsg(0202)))
124 | fmt.Scanln(&postalcode)
125 |
126 | sd.Req.Parameter = fmt.Sprintf("?country=%s&postalcode=%s", entry.ShortName, postalcode)
127 |
128 | err = sd.Headends()
129 |
130 | if err == nil {
131 | break
132 | }
133 |
134 | }
135 |
136 | // Select Linup
137 | index = 0
138 |
139 | menu.Entry = make(map[int]Entry)
140 | menu.Select = getMsg(0203)
141 |
142 | // Cancel
143 | entry.Key = index
144 | entry.Value = getMsg(0200)
145 | menu.Entry[index] = entry
146 |
147 | for _, slice := range sd.Resp.Headend {
148 |
149 | for _, lineup := range slice.Lineups {
150 |
151 | index++
152 | entry.Key = index
153 | entry.Value = fmt.Sprintf("%s [%s]", lineup.Name, lineup.Lineup)
154 | entry.Lineup = lineup.Lineup
155 |
156 | menu.Entry[index] = entry
157 |
158 | }
159 |
160 | }
161 |
162 | selection = menu.Show()
163 |
164 | switch selection {
165 |
166 | case 0:
167 | return
168 | default:
169 | entry = menu.Entry[selection]
170 |
171 | }
172 |
173 | sd.Req.Parameter = fmt.Sprintf("/%s", entry.Lineup)
174 | sd.Req.Type = "PUT"
175 |
176 | err = sd.Lineups()
177 |
178 | return
179 | }
180 |
181 | func (e *Entry) removeLineup(sd *SD) (err error) {
182 |
183 | var index, selection int
184 | var menu Menu
185 | var entry Entry
186 |
187 | menu.Entry = make(map[int]Entry)
188 | menu.Select = getMsg(0204)
189 | menu.Headline = e.Value
190 |
191 | // Cancel
192 | entry.Key = index
193 | entry.Value = getMsg(0200)
194 | menu.Entry[index] = entry
195 |
196 | for _, lineup := range sd.Resp.Status.Lineups {
197 |
198 | index++
199 | entry.Key = index
200 | entry.Value = fmt.Sprintf("%s [%s]", lineup.Name, lineup.Lineup)
201 | entry.Lineup = lineup.Lineup
202 |
203 | menu.Entry[index] = entry
204 |
205 | }
206 |
207 | selection = menu.Show()
208 |
209 | switch selection {
210 |
211 | case 0:
212 | return
213 |
214 | default:
215 | entry = menu.Entry[selection]
216 |
217 | }
218 |
219 | sd.Req.Parameter = fmt.Sprintf("/%s", entry.Lineup)
220 | sd.Req.Type = "DELETE"
221 |
222 | err = sd.Lineups()
223 |
224 | return
225 | }
226 |
--------------------------------------------------------------------------------
/sd.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | )
10 | var Token string
11 | // Init : Init Schedules Direct
12 | func (sd *SD) Init() (err error) {
13 |
14 | sd.BaseURL = "https://json.schedulesdirect.org/20141201/"
15 |
16 | sd.Login = func() (err error) {
17 |
18 | sd.Req.URL = sd.BaseURL + "token"
19 | sd.Req.Type = "POST"
20 | sd.Req.Call = "login"
21 | sd.Req.Compression = false
22 | sd.Token = ""
23 |
24 | var login = Config.Account
25 |
26 | sd.Req.Data, err = json.MarshalIndent(login, "", " ")
27 | if err != nil {
28 | ShowErr(err)
29 | return
30 | }
31 |
32 | err = sd.Connect()
33 | if err != nil {
34 |
35 | if sd.Resp.Login.Code != 0 {
36 | // SD Account problem
37 | return
38 | }
39 |
40 | return
41 | }
42 |
43 | showInfo("SD", fmt.Sprintf("Login...%s", sd.Resp.Login.Message))
44 |
45 | sd.Token = sd.Resp.Login.Token
46 | Token = sd.Token
47 | return
48 | }
49 |
50 | sd.Status = func() (err error) {
51 |
52 | fmt.Println()
53 |
54 | sd.Req.URL = sd.BaseURL + "status"
55 | sd.Req.Type = "GET"
56 | sd.Req.Data = nil
57 | sd.Req.Call = "status"
58 | sd.Req.Compression = false
59 |
60 | err = sd.Connect()
61 | if err != nil {
62 | return
63 | }
64 |
65 | showInfo("SD", fmt.Sprintf("Account Expires: %v", sd.Resp.Status.Account.Expires))
66 | showInfo("SD", fmt.Sprintf("Lineups: %d / %d", len(sd.Resp.Status.Lineups), sd.Resp.Status.Account.MaxLineups))
67 |
68 | for _, status := range sd.Resp.Status.SystemStatus {
69 | showInfo("SD", fmt.Sprintf("System Status: %s [%s]", status.Status, status.Message))
70 | }
71 |
72 | showInfo("G2G", fmt.Sprintf("Channels: %d", len(Config.Station)))
73 |
74 | return
75 | }
76 |
77 | sd.Countries = func() (err error) {
78 |
79 | sd.Req.URL = sd.BaseURL + "available/countries"
80 | sd.Req.Type = "GET"
81 | sd.Req.Data = nil
82 | sd.Req.Call = "countries"
83 | sd.Req.Compression = false
84 |
85 | err = sd.Connect()
86 | if err != nil {
87 | return
88 | }
89 |
90 | return
91 | }
92 |
93 | sd.Headends = func() (err error) {
94 |
95 | sd.Req.URL = fmt.Sprintf("%sheadends%s", sd.BaseURL, sd.Req.Parameter)
96 | sd.Req.Type = "GET"
97 | sd.Req.Data = nil
98 | sd.Req.Call = "headends"
99 | sd.Req.Compression = false
100 |
101 | err = sd.Connect()
102 | if err != nil {
103 | return
104 | }
105 |
106 | return
107 | }
108 |
109 | sd.Lineups = func() (err error) {
110 |
111 | sd.Req.URL = fmt.Sprintf("%slineups%s", sd.BaseURL, sd.Req.Parameter)
112 | sd.Req.Data = nil
113 | sd.Req.Call = "lineups"
114 | sd.Req.Compression = false
115 |
116 | err = sd.Connect()
117 | if err != nil {
118 | return
119 | }
120 |
121 | if len(sd.Resp.Lineup.Message) != 0 {
122 | showInfo("SD", sd.Resp.Lineup.Message)
123 | }
124 |
125 | return
126 | }
127 |
128 | sd.Schedule = func() (err error) {
129 |
130 | sd.Req.URL = fmt.Sprintf("%sschedules", sd.BaseURL)
131 | sd.Req.Type = "POST"
132 | sd.Req.Call = "schedule"
133 | sd.Req.Compression = false
134 |
135 | err = sd.Connect()
136 | if err != nil {
137 | return
138 | }
139 |
140 | return
141 | }
142 |
143 | sd.Program = func() (err error) {
144 |
145 | sd.Req.Type = "POST"
146 | sd.Req.Call = "program"
147 | sd.Req.Compression = true
148 |
149 | err = sd.Connect()
150 | if err != nil {
151 | return
152 | }
153 |
154 | return
155 | }
156 |
157 | return
158 | }
159 |
160 | // Connect : Connect to Schedules Direct
161 |
162 | func (sd *SD) Connect() (err error) {
163 |
164 | var sdStatus SDStatus
165 |
166 | showInfo("URL", sd.Req.URL)
167 |
168 | req, err := http.NewRequest(sd.Req.Type, sd.Req.URL, bytes.NewBuffer(sd.Req.Data))
169 | if err != nil {
170 | return
171 | }
172 |
173 | if sd.Req.Compression {
174 | req.Header.Set("Accept-Encoding", "deflate,gzip")
175 | }
176 |
177 | req.Header.Set("Token", sd.Token)
178 | req.Header.Set("User-Agent", AppName)
179 | req.Header.Set("X-Custom-Header", AppName)
180 | req.Header.Set("Content-Type", "application/json")
181 |
182 | client := &http.Client{}
183 | resp, err := client.Do(req)
184 | if err != nil {
185 | ShowErr(err)
186 | return
187 | }
188 | defer resp.Body.Close()
189 |
190 | body, err := ioutil.ReadAll(resp.Body)
191 | if err != nil {
192 | ShowErr(err)
193 | return
194 | }
195 |
196 | sd.Resp.Body = body
197 |
198 | switch sd.Req.Call {
199 |
200 | case "login":
201 | err = json.Unmarshal(body, &sd.Resp.Login)
202 | if err != nil {
203 | ShowErr(err)
204 | }
205 |
206 | sdStatus.Code = sd.Resp.Login.Code
207 | sdStatus.Message = sd.Resp.Login.Message
208 |
209 | case "status":
210 | err = json.Unmarshal(body, &sd.Resp.Status)
211 | if err != nil {
212 | ShowErr(err)
213 | }
214 |
215 | sdStatus.Code = sd.Resp.Status.Code
216 | sdStatus.Message = sd.Resp.Status.Message
217 |
218 | case "countries":
219 | err = json.Unmarshal(body, &sd.Resp.Countries)
220 | if err != nil {
221 | ShowErr(err)
222 | }
223 |
224 | case "headends":
225 | err = json.Unmarshal(body, &sd.Resp.Headend)
226 | if err != nil {
227 | ShowErr(err)
228 | }
229 |
230 | case "lineups":
231 | err = json.Unmarshal(body, &sd.Resp.Lineup)
232 | if err != nil {
233 | ShowErr(err)
234 | }
235 | sd.Resp.Body = body
236 |
237 | sdStatus.Code = sd.Resp.Lineup.Code
238 | sdStatus.Message = sd.Resp.Lineup.Message
239 |
240 | case "schedule", "program":
241 | sd.Resp.Body = body
242 |
243 | }
244 |
245 | switch sdStatus.Code {
246 |
247 | case 0:
248 | //showInfo("SD", sd.Res.Message)
249 |
250 | default:
251 | err = fmt.Errorf("%s [SD API Error Code: %d]", sdStatus.Message, sdStatus.Code)
252 | ShowErr(err)
253 |
254 | }
255 |
256 | return
257 | }
258 |
--------------------------------------------------------------------------------
/xmltv.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "fmt"
7 | "io/ioutil"
8 | "path/filepath"
9 | "runtime"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // CreateXMLTV : Create XMLTV file from cache file
15 | func CreateXMLTV(filename string) (err error) {
16 |
17 | defer func() {
18 | runtime.GC()
19 | }()
20 |
21 | Config.File = strings.TrimSuffix(filename, filepath.Ext(filename))
22 |
23 | var generator xml.Attr
24 | generator.Name = xml.Name{Local: AppName}
25 | generator.Value = AppName
26 |
27 | var source xml.Attr
28 | source.Name = xml.Name{Local: "source-info-name"}
29 | source.Value = "Schedules Direct"
30 |
31 | var info xml.Attr
32 | info.Name = xml.Name{Local: "source-info-url"}
33 | info.Value = "http://schedulesdirect.org"
34 |
35 | buf := &bytes.Buffer{}
36 | buf.WriteString(xml.Header)
37 |
38 | enc := xml.NewEncoder(buf)
39 | enc.Indent("", " ")
40 |
41 | var he = func(err error) {
42 | if err != nil {
43 | ShowErr(err)
44 | return
45 | }
46 | }
47 |
48 | err = Config.Open()
49 | if err != nil {
50 | return
51 | }
52 |
53 | err = Cache.Open()
54 | if err != nil {
55 | return
56 | }
57 |
58 | Cache.Init()
59 | err = Cache.Open()
60 | if err != nil {
61 | ShowErr(err)
62 | return
63 | }
64 |
65 | showInfo("G2G", fmt.Sprintf("Create XMLTV File [%s]", Config.Files.XMLTV))
66 |
67 | he(enc.EncodeToken(xml.StartElement{Name: xml.Name{Local: "tv"}, Attr: []xml.Attr{generator, source, info}}))
68 |
69 | // XMLTV Channels
70 | for _, cache := range Cache.Channel {
71 |
72 | var xmlCha channel // struct_config.go
73 |
74 | xmlCha.ID = fmt.Sprintf("%s.%s.schedulesdirect.org", AppName, cache.StationID)
75 | xmlCha.Icon = cache.getLogo()
76 | xmlCha.DisplayName = append(xmlCha.DisplayName, DisplayName{Value: cache.Callsign})
77 | xmlCha.DisplayName = append(xmlCha.DisplayName, DisplayName{Value: cache.Name})
78 |
79 | he(enc.Encode(xmlCha))
80 |
81 | }
82 |
83 | // XMLTV Programs
84 | for _, cache := range Cache.Channel {
85 |
86 | var program = getProgram(cache)
87 | he(enc.Encode(program))
88 |
89 | }
90 |
91 | he(enc.EncodeToken(xml.EndElement{Name: xml.Name{Local: "tv"}}))
92 | he(enc.Flush())
93 |
94 | // write the whole body at once
95 | err = ioutil.WriteFile(Config.Files.XMLTV, buf.Bytes(), 0644)
96 | if err != nil {
97 | panic(err)
98 | }
99 |
100 | return
101 | }
102 |
103 | // Channel infos
104 | func (channel *G2GCache) getLogo() (icon Icon) {
105 |
106 | icon.Src = channel.Logo.URL
107 | icon.Height = channel.Logo.Height
108 | icon.Width = channel.Logo.Width
109 |
110 | return
111 | }
112 |
113 | func getProgram(channel G2GCache) (p []Programme) {
114 |
115 | if schedule, ok := Cache.Schedule[channel.StationID]; ok {
116 |
117 | for _, s := range schedule {
118 |
119 | var pro Programme
120 |
121 | var countryCode = Config.GetLineupCountry(channel.StationID)
122 |
123 | // Channel ID
124 | pro.Channel = fmt.Sprintf("%s.%s.schedulesdirect.org", AppName, channel.StationID)
125 |
126 | // Start and Stop time
127 | timeLayout := "2006-01-02 15:04:05 +0000 UTC"
128 | t, err := time.Parse(timeLayout, s.AirDateTime.Format(timeLayout))
129 | if err != nil {
130 | ShowErr(err)
131 | return
132 | }
133 |
134 | var dateArray = strings.Fields(t.String())
135 | var offset = " " + dateArray[2]
136 | var startTime = t.Format("20060102150405") + offset
137 | var stopTime = t.Add(time.Second*time.Duration(s.Duration)).Format("20060102150405") + offset
138 | pro.Start = startTime
139 | pro.Stop = stopTime
140 |
141 | // Title
142 | var lang = "en"
143 | if len(channel.BroadcastLanguage) != 0 {
144 | lang = channel.BroadcastLanguage[0]
145 | }
146 |
147 | // New and Live guide mini-icons
148 | pro.Title = Cache.GetTitle(s.ProgramID, lang)
149 | if s.LiveTapeDelay == "Live"{
150 | pro.Title[0].Value = pro.Title[0].Value + " ᴸᶦᵛᵉ"
151 | }
152 | if s.New && s.LiveTapeDelay != "Live"{
153 | pro.Title[0].Value = pro.Title[0].Value + " ᴺᵉʷ"
154 | }
155 |
156 |
157 | // Sub Title
158 | pro.SubTitle = Cache.GetSubTitle(s.ProgramID, lang)
159 |
160 | // Description
161 | pro.Desc = Cache.GetDescs(s.ProgramID, pro.SubTitle.Value)
162 |
163 | // Credits
164 | pro.Credits = Cache.GetCredits(s.ProgramID)
165 |
166 | // Category
167 | pro.Categorys = Cache.GetCategory(s.ProgramID)
168 |
169 | // Language
170 | pro.Language = lang
171 |
172 | // EpisodeNum
173 | pro.EpisodeNums = Cache.GetEpisodeNum(s.ProgramID)
174 |
175 | // Icon
176 | pro.Icon = Cache.GetIcon(s.ProgramID[0:10])
177 |
178 | // Rating
179 | pro.Rating = Cache.GetRating(s.ProgramID, countryCode)
180 |
181 | // Video
182 | for _, v := range s.VideoProperties {
183 |
184 | switch strings.ToLower(v) {
185 |
186 | case "hdtv", "sdtv", "uhdtv", "3d":
187 | pro.Video.Quality = strings.ToUpper(v)
188 |
189 | }
190 |
191 | }
192 |
193 | // Audio
194 | for _, a := range s.AudioProperties {
195 |
196 | switch a {
197 |
198 | case "stereo", "dvs":
199 | pro.Audio.Stereo = "stereo"
200 | case "DD 5.1", "Atmos":
201 | pro.Audio.Stereo = "dolby digital"
202 | case "Dolby":
203 | pro.Audio.Stereo = "dolby"
204 | case "dubbed", "mono":
205 | pro.Audio.Stereo = "mono"
206 | default:
207 | pro.Audio.Stereo = "mono"
208 |
209 | }
210 |
211 | }
212 |
213 | // New / PreviouslyShown
214 | if s.New {
215 | pro.New = &New{Value: ""}
216 | } else {
217 | pro.PreviouslyShown = Cache.GetPreviouslyShown(s.ProgramID)
218 | }
219 |
220 | // Live
221 | if s.LiveTapeDelay == "Live" {
222 | pro.Live = &Live{Value: ""}
223 | }
224 |
225 | p = append(p, pro)
226 |
227 | }
228 |
229 | }
230 |
231 | return
232 | }
233 |
--------------------------------------------------------------------------------
/configure.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | "gopkg.in/yaml.v3"
12 | )
13 |
14 | // Configure : Configure config file
15 | func Configure(filename string) (err error) {
16 |
17 | var menu Menu
18 | var entry Entry
19 | var sd SD
20 |
21 | Config.File = strings.TrimSuffix(filename, filepath.Ext(filename))
22 |
23 | err = Config.Open()
24 | if err != nil {
25 | return
26 | }
27 |
28 | sd.Init()
29 |
30 | if len(Config.Account.Username) != 0 || len(Config.Account.Password) != 0 {
31 | sd.Login()
32 | sd.Status()
33 | }
34 |
35 | for {
36 |
37 | menu.Entry = make(map[int]Entry)
38 |
39 | menu.Headline = fmt.Sprintf("%s [%s.yaml]", getMsg(0000), Config.File)
40 | menu.Select = getMsg(0001)
41 |
42 | // Exit
43 | entry.Key = 0
44 | entry.Value = getMsg(0010)
45 | menu.Entry[0] = entry
46 |
47 | // Account
48 | entry.Key = 1
49 | entry.Value = getMsg(0011)
50 | menu.Entry[1] = entry
51 | if len(Config.Account.Username) == 0 || len(Config.Account.Password) == 0 {
52 | entry.account()
53 | err = sd.Login()
54 | if err != nil {
55 | os.RemoveAll(Config.File + ".yaml")
56 | os.Exit(0)
57 | }
58 | sd.Status()
59 |
60 | }
61 |
62 | // Add Lineup
63 | entry.Key = 2
64 | entry.Value = getMsg(0012)
65 | menu.Entry[2] = entry
66 |
67 | // Remove Lineup
68 | entry.Key = 3
69 | entry.Value = getMsg(0013)
70 | menu.Entry[3] = entry
71 |
72 | // Manage Channels
73 | entry.Key = 4
74 | entry.Value = getMsg(0014)
75 | menu.Entry[4] = entry
76 |
77 | // Create XMLTV file
78 | entry.Key = 5
79 | entry.Value = fmt.Sprintf("%s [%s]", getMsg(0016), Config.Files.XMLTV)
80 | menu.Entry[5] = entry
81 |
82 | var selection = menu.Show()
83 |
84 | entry = menu.Entry[selection]
85 |
86 | switch selection {
87 |
88 | case 0:
89 | Config.Save()
90 | os.Exit(0)
91 |
92 | case 1:
93 | entry.account()
94 | sd.Login()
95 | sd.Status()
96 |
97 | case 2:
98 | entry.addLineup(&sd)
99 | sd.Status()
100 |
101 | case 3:
102 | entry.removeLineup(&sd)
103 | sd.Status()
104 |
105 | case 4:
106 | entry.manageChannels(&sd)
107 | sd.Status()
108 |
109 | case 5:
110 | sd.Update(filename)
111 |
112 | }
113 |
114 | }
115 |
116 | }
117 |
118 | func (c *config) Open() (err error) {
119 |
120 | data, err := ioutil.ReadFile(fmt.Sprintf("%s.yaml", c.File))
121 | var rmCacheFile, newOptions bool
122 |
123 | if err != nil {
124 | // File is missing, create new config file (YAML)
125 | c.InitConfig()
126 | err = c.Save()
127 | if err != nil {
128 | return
129 | }
130 |
131 | return nil
132 | }
133 |
134 | // Open config file and convert Yaml to Struct (config)
135 | err = yaml.Unmarshal(data, &c)
136 | if err != nil {
137 | return
138 | }
139 |
140 | /*
141 | New config options
142 | */
143 |
144 | // Credits tag
145 | if !bytes.Contains(data, []byte("credits tag")) {
146 |
147 | rmCacheFile = true
148 | newOptions = true
149 |
150 | Config.Options.Credits = true
151 |
152 | showInfo("G2G", fmt.Sprintf("%s (credits) [%s]", getMsg(0300), Config.File))
153 |
154 | }
155 |
156 | // Rating tag
157 | if !bytes.Contains(data, []byte("Rating:")) {
158 |
159 | newOptions = true
160 |
161 | Config.Options.Rating.Guidelines = true
162 | Config.Options.Rating.Countries = []string{}
163 | Config.Options.Rating.CountryCodeAsSystem = false
164 | Config.Options.Rating.MaxEntries = 1
165 |
166 | showInfo("G2G", fmt.Sprintf("%s (rating) [%s]", getMsg(0300), Config.File))
167 |
168 | }
169 | // Download Images from TV Shows
170 | if !bytes.Contains(data, []byte("Local Images Cache:")) {
171 |
172 | newOptions = true
173 |
174 | Config.Options.TVShowImages = false
175 |
176 | showInfo("G2G", fmt.Sprintf("%s (Local Images Cache) [%s]", getMsg(401), Config.File))
177 |
178 | }
179 |
180 | // Download Images from TV Shows
181 | if !bytes.Contains(data, []byte("Images Path:")) {
182 |
183 | newOptions = true
184 |
185 | Config.Options.ImagesPath = "${images_path}"
186 | showInfo("G2G", fmt.Sprintf("%s (TVShows images Path) [%s]", getMsg(403), Config.File))
187 |
188 | }
189 | // Proxy scheduledirect images url
190 | if !bytes.Contains(data, []byte("Proxy Images")) {
191 | newOptions = true
192 | Config.Options.ProxyImages = false
193 | }
194 |
195 | // Hostname
196 | if !bytes.Contains(data, []byte("Hostname")) {
197 | newOptions = true
198 | Config.Options.Hostname = "localhost:8080"
199 | }
200 |
201 | // SD errors
202 | if !bytes.Contains(data, []byte("download errors")) {
203 |
204 | newOptions = true
205 | Config.Options.SDDownloadErrors = false
206 |
207 | showInfo("G2G", fmt.Sprintf("%s (SD errors) [%s]", getMsg(0300), Config.File))
208 |
209 | }
210 |
211 | if newOptions {
212 |
213 | err = c.Save()
214 | if err != nil {
215 | return
216 | }
217 |
218 | }
219 |
220 | if rmCacheFile {
221 | Cache.Remove()
222 | }
223 |
224 | return
225 | }
226 |
227 | func (c *config) Save() (err error) {
228 |
229 | data, err := yaml.Marshal(&c)
230 | if err != nil {
231 | return err
232 | }
233 |
234 | err = ioutil.WriteFile(fmt.Sprintf("%s.yaml", c.File), data, 0644)
235 | if err != nil {
236 | return
237 | }
238 |
239 | return
240 | }
241 |
242 | func (c *config) InitConfig() {
243 |
244 | // Files
245 | c.Files.Cache = fmt.Sprintf("%s_cache.json", c.File)
246 | c.Files.XMLTV = fmt.Sprintf("%s.xml", c.File)
247 |
248 | // Options
249 | c.Options.PosterAspect = "all"
250 | c.Options.TVShowImages = false
251 | c.Options.Schedule = 7
252 | c.Options.SubtitleIntoDescription = false
253 | c.Options.Credits = false
254 | c.Options.ImagesPath = "/data/images/"
255 | c.Options.ProxyImages = false
256 | Config.Options.Rating.Guidelines = true
257 | Config.Options.Rating.Countries = []string{"USA", "CHE", "DE"}
258 | Config.Options.Rating.CountryCodeAsSystem = false
259 | Config.Options.Rating.MaxEntries = 1
260 |
261 | }
262 |
263 | func (c *config) GetChannelList(lineup string) (list []string) {
264 |
265 | for _, channel := range c.Station {
266 |
267 | switch len(lineup) {
268 |
269 | case 0:
270 | list = append(list, channel.ID)
271 |
272 | default:
273 | if lineup == channel.Lineup {
274 | list = append(list, channel.ID)
275 | }
276 |
277 | }
278 |
279 | }
280 |
281 | return
282 | }
283 |
284 | func (c *config) GetLineupCountry(id string) (countryCode string) {
285 |
286 | for _, channel := range c.Station {
287 |
288 | if id == channel.ID {
289 | countryCode = strings.Split(channel.Lineup, "-")[0]
290 | return
291 | }
292 |
293 | }
294 |
295 | return
296 | }
297 |
--------------------------------------------------------------------------------
/struct_sd.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | type SDStatus struct {
6 | Message string `json:"message"`
7 | Code int `json:"code"`
8 | }
9 |
10 | // SD : Schedules Direct API
11 | type SD struct {
12 | BaseURL string
13 | Token string
14 |
15 | // SD Request
16 | Req struct {
17 | URL string
18 | Data []byte
19 | Type string
20 | Compression bool
21 | Parameter string
22 | Call string
23 | }
24 |
25 | // SD Response
26 | Resp struct {
27 | Body []byte
28 |
29 | // Login
30 | Login struct {
31 | Message string `json:"message"`
32 | Code int `json:"code"`
33 | ServerID string `json:"serverID"`
34 | Datetime time.Time `json:"datetime"`
35 |
36 | Token string `json:"token"`
37 | }
38 |
39 | // Status
40 | Status struct {
41 | Account struct {
42 | Expires time.Time `json:"expires"`
43 | MaxLineups int64 `json:"maxLineups"`
44 | Messages []interface{} `json:"messages"`
45 | } `json:"account"`
46 | Code int `json:"code"`
47 | Message string `json:"message"`
48 |
49 | Datetime string `json:"datetime"`
50 | LastDataUpdate string `json:"lastDataUpdate"`
51 | Lineups []struct {
52 | Lineup string `json:"lineup"`
53 | Modified string `json:"modified"`
54 | Name string `json:"name"`
55 | URI string `json:"uri"`
56 | } `json:"lineups"`
57 | Notifications []interface{} `json:"notifications"`
58 | ServerID string `json:"serverID"`
59 | SystemStatus []struct {
60 | Date string `json:"date"`
61 | Message string `json:"message"`
62 | Status string `json:"status"`
63 | } `json:"systemStatus"`
64 | }
65 |
66 | // Countries
67 | Countries struct {
68 | Caribbean []struct {
69 | FullName string `json:"fullName"`
70 | OnePostalCode bool `json:"onePostalCode"`
71 | PostalCode string `json:"postalCode"`
72 | PostalCodeExample string `json:"postalCodeExample"`
73 | ShortName string `json:"shortName"`
74 | } `json:"Caribbean"`
75 | Europe []struct {
76 | FullName string `json:"fullName"`
77 | OnePostalCode bool `json:"onePostalCode"`
78 | PostalCode string `json:"postalCode"`
79 | PostalCodeExample string `json:"postalCodeExample"`
80 | ShortName string `json:"shortName"`
81 | } `json:"Europe"`
82 | LatinAmerica []struct {
83 | FullName string `json:"fullName"`
84 | OnePostalCode bool `json:"onePostalCode"`
85 | PostalCode string `json:"postalCode"`
86 | PostalCodeExample string `json:"postalCodeExample"`
87 | ShortName string `json:"shortName"`
88 | } `json:"Latin America"`
89 | NorthAmerica []struct {
90 | FullName string `json:"fullName"`
91 | PostalCode string `json:"postalCode"`
92 | PostalCodeExample string `json:"postalCodeExample"`
93 | ShortName string `json:"shortName"`
94 | } `json:"North America"`
95 | Zzz []struct {
96 | FullName string `json:"fullName"`
97 | OnePostalCode bool `json:"onePostalCode"`
98 | PostalCode string `json:"postalCode"`
99 | PostalCodeExample string `json:"postalCodeExample"`
100 | ShortName string `json:"shortName"`
101 | } `json:"ZZZ"`
102 | }
103 |
104 | // Headend
105 | Headend []struct {
106 | Headend string `json:"headend"`
107 | Lineups []struct {
108 | Lineup string `json:"lineup"`
109 | Name string `json:"name"`
110 | URI string `json:"uri"`
111 | } `json:"lineups"`
112 | Location string `json:"location"`
113 | Transport string `json:"transport"`
114 | }
115 |
116 | // Lineup
117 | Lineup struct {
118 | // PUT
119 | ChangesRemaining int `json:"changesRemaining"`
120 | Code int `json:"code"`
121 | Datetime time.Time `json:"datetime"`
122 | Message string `json:"message"`
123 | Response string `json:"response"`
124 | ServerID string `json:"serverID"`
125 |
126 | // GET
127 | Map []struct {
128 | StationID string `json:"stationID"`
129 | Channel string `json:"channel"`
130 | } `json:"map"`
131 | Stations []Station `json:"stations"`
132 | }
133 | }
134 |
135 | // SD API Calls
136 | Login func() (err error)
137 | Status func() (err error)
138 | Countries func() (err error)
139 | Headends func() (err error)
140 | Lineups func() (err error)
141 | Delete func() (err error)
142 | Channels func() (err error)
143 | Schedule func() (err error)
144 | Program func() (err error)
145 | }
146 |
147 | // Station : Station SD API
148 | type Station struct {
149 | StationID string `json:"stationID"`
150 | Name string `json:"name"`
151 | Callsign string `json:"callsign"`
152 | Affiliate string `json:"affiliate,omitempty"`
153 | BroadcastLanguage []string `json:"broadcastLanguage"`
154 | DescriptionLanguage []string `json:"descriptionLanguage"`
155 | StationLogo []struct {
156 | URL string `json:"URL"`
157 | Height int `json:"height"`
158 | Width int `json:"width"`
159 | Md5 string `json:"md5"`
160 | Source string `json:"source"`
161 | } `json:"stationLogo,omitempty"`
162 | Logo struct {
163 | URL string `json:"URL"`
164 | Height int `json:"height"`
165 | Width int `json:"width"`
166 | Md5 string `json:"md5"`
167 | } `json:"logo,omitempty"`
168 | Broadcaster struct {
169 | City string `json:"city"`
170 | State string `json:"state"`
171 | Postalcode string `json:"postalcode"`
172 | Country string `json:"country"`
173 | } `json:"broadcaster,omitempty"`
174 | }
175 |
176 | // SDProgram : Schedules Direct program data
177 | type SDProgram struct {
178 |
179 | // Program
180 | Cast []struct {
181 | BillingOrder string `json:"billingOrder"`
182 | CharacterName string `json:"characterName"`
183 | Name string `json:"name"`
184 | NameID string `json:"nameId"`
185 | PersonID string `json:"personId"`
186 | Role string `json:"role"`
187 | } `json:"cast"`
188 | ContentAdvisory []string `json:"contentAdvisory"`
189 | ContentRating []struct {
190 | Body string `json:"body"`
191 | Code string `json:"code"`
192 | Country string `json:"country"`
193 | } `json:"contentRating"`
194 | Crew []struct {
195 | BillingOrder string `json:"billingOrder"`
196 | Name string `json:"name"`
197 | NameID string `json:"nameId"`
198 | PersonID string `json:"personId"`
199 | Role string `json:"role"`
200 | } `json:"crew"`
201 | Descriptions struct {
202 | Description1000 []struct {
203 | Description string `json:"description"`
204 | DescriptionLanguage string `json:"descriptionLanguage"`
205 | } `json:"description1000"`
206 | Description100 []struct {
207 | DescriptionLanguage string `json:"descriptionLanguage"`
208 | Description string `json:"description"`
209 | } `json:"description100"`
210 | } `json:"descriptions"`
211 | EntityType string `json:"entityType"`
212 | EpisodeTitle150 string `json:"episodeTitle150"`
213 | Genres []string `json:"genres"`
214 | HasEpisodeArtwork bool `json:"hasEpisodeArtwork"`
215 | HasImageArtwork bool `json:"hasImageArtwork"`
216 | HasSeriesArtwork bool `json:"hasSeriesArtwork"`
217 | Md5 string `json:"md5"`
218 |
219 | Metadata []struct {
220 | Gracenote struct {
221 | Episode int `json:"episode"`
222 | Season int `json:"season"`
223 | } `json:"Gracenote"`
224 | } `json:"metadata"`
225 |
226 | OriginalAirDate string `json:"originalAirDate"`
227 | ProgramID string `json:"programID"`
228 | ResourceID string `json:"resourceID"`
229 | ShowType string `json:"showType"`
230 | Titles []struct {
231 | Title120 string `json:"title120"`
232 | } `json:"titles"`
233 | }
234 |
235 | //SDMetadata : Schedules Direct meta data
236 | type SDMetadata struct {
237 | Data []Data `json:"data",required`
238 | ProgramID string `json:"programID"`
239 | }
240 |
241 | type Data struct {
242 | Aspect string `json:"aspect"`
243 | Height string `json:"height"`
244 | Size string `json:"size"`
245 | URI string `json:"uri"`
246 | Width string `json:"width"`
247 | Category string `json:"category"`
248 | Tier string `json:"tier"`
249 | }
250 |
251 | // SDStation : Schedules Direct stations
252 | type SDStation struct {
253 | Map []struct {
254 | Channel string `json:"channel"`
255 | StationID string `json:"stationID"`
256 | } `json:"map"`
257 | Metadata struct {
258 | Lineup string `json:"lineup"`
259 | Modified string `json:"modified"`
260 | Transport string `json:"transport"`
261 | } `json:"metadata"`
262 | Stations []struct {
263 | Affiliate string `json:"affiliate"`
264 | BroadcastLanguage []string `json:"broadcastLanguage"`
265 | Broadcaster struct {
266 | City string `json:"city"`
267 | Country string `json:"country"`
268 | Postalcode string `json:"postalcode"`
269 | State string `json:"state"`
270 | } `json:"broadcaster"`
271 | Callsign string `json:"callsign"`
272 | DescriptionLanguage []string `json:"descriptionLanguage"`
273 | Logo struct {
274 | URL string `json:"URL"`
275 | Height int `json:"height"`
276 | Width int `json:"width"`
277 | Md5 string `json:"md5"`
278 | } `json:"logo,omitempty"`
279 | Name string `json:"name"`
280 | StationID string `json:"stationID"`
281 | StationLogo []struct {
282 | URL string `json:"URL"`
283 | Height int `json:"height"`
284 | Md5 string `json:"md5"`
285 | Source string `json:"source"`
286 | Width int `json:"width"`
287 | } `json:"stationLogo"`
288 | } `json:"stations"`
289 | }
290 |
291 | // SDError : Errors from SD
292 | type SDError struct {
293 | Data struct {
294 | Code int64 `json:"code"`
295 | Datetime string `json:"datetime"`
296 | Message string `json:"message"`
297 | Response string `json:"response"`
298 | ServerID string `json:"serverID"`
299 | } `json:"data"`
300 | ProgramID string `json:"programID"`
301 | }
302 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Recent changes by [Chuchodavids](https://github.com/mar-mei/guide2go/issues?q=is%3Apr+author%3AChuchodavids):**
2 | 1. Grab images from Schedules Direct in a local folder. This script will download tvshow/movie images and save them in a local folder. Then it will expose the images from a go server. This is useful for players like Emby or Plex.
3 | 1. For players like tiviMate I added the "live" and "new" icon in the tittle of the program. This is usually auto-added in Emby or other IPTV players, but TiviMate does not do this by reading XML tags.
4 | 1. Added the option to proxy the images instead of downloading them locally. It will pass the guide2go server IP and the server will act as a reverse-proxy to Schedules Direct to grab the image. (only usefully for small EPG files).
5 |
6 |
7 | ## Guide2Go
8 | Guide2Go is written in Go and creates an XMLTV file from the Schedules Direct JSON API.
9 | **Configuration files from version 1.0.6 or earlier are not compatible!**
10 |
11 | ### Advantages compared to version 1.0.x
12 | - 3x faster
13 | - Less memory
14 | - Smaller cache file
15 |
16 | #### Features
17 | - Cache function to download only new EPG data
18 | - No database is required
19 | - Update EPG with CLI command for using your own scripts
20 |
21 | #### Requirement
22 | - [Schedules Direct](https://www.schedulesdirect.org/ "Schedules Direct") Account
23 | - [Go](https://golang.org/ "Golang") to build the binary
24 | - Computer with 1-2 GB memory
25 |
26 |
27 | ## Installation
28 | ### Build binary
29 | The following command must be executed with the terminal / command prompt inside the source code folder.
30 | Linux, Unix, OSX:
31 | ```
32 | go build -o guide2go
33 | ```
34 | Windows:
35 |
36 | ```
37 | go build -o guide2go.exe
38 | ```
39 | ### Docker
40 | Download the docker image:
41 |
42 | #### Volumes
43 |
44 | - /data/images -- Path where images will be cached. It has to be the same that was declared in the Environment variables
45 | - /data/livetv -- optional -- If you want to save the files in a directory that other applications will have access to. (shared folder)
46 |
47 | **Stable:**
48 |
49 | `docker pull chuchodavids/guide2go:stable`
50 |
51 | **Development:**
52 |
53 | `docker pull chuchodavids/guide2go:development`
54 |
55 | **docker-compose**
56 | ```
57 | version: "3.4"
58 | services:
59 | guide2go:
60 | container_name: guide2go
61 | image: chuchodavids/guide2go:stable
62 | ports:
63 | - 8080:8080
64 | environment:
65 | - TZ: America/Chicago
66 | volumes:
67 | - /data/config/guide2go:/config
68 | - /data/livetv/:/data/livetv/
69 | - /data/images:/data/images/
70 | restart: always
71 | ```
72 |
73 | Switch -o creates the binary *guide2go* in the current folder.
74 |
75 |
76 | ### Show CLI parameter:
77 | ```guide2go -h```
78 |
79 | ```
80 | -config string
81 | = Get data from Schedules Direct with configuration file. [filename.yaml]
82 | -configure string
83 | = Create or modify the configuration file. [filename.yaml]
84 | -h : Show help
85 | ```
86 |
87 | ### Create a config file:
88 |
89 | **note: You can use the sample config file that is in the /config folder inside of the docker container**
90 |
91 | ```guide2go -configure MY_CONFIG_FILE.yaml```
92 | If the configuration file does not exist, a YAML configuration file is created.
93 |
94 | **Configuration file from version 1.0.6 or earlier is not compatible.**
95 | ##### Terminal Output:
96 | ```
97 | 2020/05/07 12:00:00 [G2G ] Version: 1.1.2
98 | 2020/05/07 12:00:00 [URL ] https://json.schedulesdirect.org/20141201/token
99 | 2020/05/07 12:00:01 [SD ] Login...OK
100 |
101 | 2020/05/07 12:00:01 [URL ] https://json.schedulesdirect.org/20141201/status
102 | 2020/05/07 12:00:01 [SD ] Account Expires: 2020-11-2 14:08:12 +0000 UTC
103 | 2020/05/07 12:00:01 [SD ] Lineups: 4 / 4
104 | 2020/05/07 12:00:01 [SD ] System Status: Online [No known issues.]
105 | 2020/05/07 12:00:01 [G2G ] Channels: 214
106 |
107 | Configuration [MY_CONFIG_FILE.yaml]
108 | -----------------------------
109 | 1. Schedules Direct Account
110 | 2. Add Lineup
111 | 3. Remove Lineup
112 | 4. Manage Channels
113 | 5. Create XMLTV File [MY_CONFIG_FILE.xml]
114 | 0. Exit
115 |
116 | ```
117 |
118 | **Follow the instructions in the terminal**
119 |
120 | 1. Schedules Direct Account:
121 | Manage Schedules Direct credentials.
122 |
123 | 2. Add Lineup:
124 | Add Lineup into the Schedules Direct account.
125 |
126 | 3. Remove Lineup:
127 | Remove Lineup from the Schedules Direct account.
128 |
129 | 4. Manage Channels:
130 | Selection of the channels to be used.
131 | All selected channels are merged into one XML file when the XMLTV file is created.
132 | When using all channels from all lineups it is recommended to create a separate Guide2Go configuration file for each lineup.
133 | **Example:**
134 | Lineup 1:
135 | ```
136 | guide2go -configure Config_Lineup_1.yaml
137 | ```
138 | Lineup 2:
139 | ```
140 | guide2go -configure Config_Lineup_2.yaml
141 | ```
142 |
143 | 5. Create XMLTV File [MY_CONFIG_FILE.xml]:
144 | Creates the XMLTV file with the selected channels.
145 |
146 | #### The YAML configuration file can be customize with an editor.:
147 |
148 | ```yaml
149 | Account:
150 | Username: SCHEDULES_DIRECT_USERNAME
151 | Password: SCHEDULES_DIRECT_HASHED_PASSWORD
152 | Files:
153 | Cache: MY_CONFIG_FILE.json
154 | XMLTV: MY_CONFIG_FILE.xml
155 | Options:
156 | Poster Aspect: all
157 | Schedule Days: 7
158 | Subtitle into Description: false
159 | Insert credits tag into XML file: true
160 | Local Images Cache: true
161 | Images Path: /data/images/
162 | Proxy Images: false
163 | Hostname: localhost:8080
164 | Rating:
165 | Insert rating tag into XML file: true
166 | Maximum rating entries. 0 for all entries: 1
167 | Preferred countries. ISO 3166-1 alpha-3 country code. Leave empty for all systems:
168 | - DEU
169 | - CHE
170 | - USA
171 | Use country code as rating system: false
172 | Show download errors from Schedules Direct in the log: false
173 | Station:
174 | - Name: Fox Sports 1 HD
175 | ID: "82547"
176 | Lineup: USA-DITV-DEFAULT
177 | - Name: Fox Sports 2 HD
178 | ID: "59305"
179 | Lineup: USA-DITV-DEFAULT
180 | ```
181 |
182 | **- Account: (Don't change)**
183 | Schedules Direct Account data, do not change them in the configuration file.
184 |
185 | **- Flies: (Can be customized)**
186 | ```yaml
187 | Cache: /data/livetv/file.json
188 | XMLTV: /data/livetv/file.xml
189 | ```
190 |
191 | **- Options: (Can be customized)**
192 | ```yaml
193 | Poster Aspect: all
194 | ```
195 | - all: All available Image ratios are used.
196 | - 2x3: Only uses the Image / Poster in 2x3 ratio. (Used by Plex)
197 | - 4x3: Only uses the Image / Poster in 4x3 ratio.
198 | - 16x9: Only uses the Image / Poster in 16x9 ratio.
199 |
200 | **Some clients only use one image, even if there are several in the XMLTV file.**
201 |
202 | ---
203 |
204 | ```yaml
205 | Schedule Days: 7
206 | ```
207 | EPG data for the specified days. Schedules Direct has EPG data for the next 12-14 days
208 |
209 | ---
210 |
211 | ```yaml
212 | Subtitle into Description: false
213 | ```
214 | Some clients only display the description and ignore the subtitle tag from the XMLTV file.
215 |
216 | **true:** If there is a subtitle, it will be added to the description.
217 |
218 | ```XML
219 |
220 |
221 | Two and a Half Men
222 | Ich arbeite für Caligula
223 | [Ich arbeite für Caligula]
224 | Alan zieht aus, da seine Freundin Kandi und er in Las Vegas eine Million Dollar gewonnen haben. Charlie kehrt zu seinem ausschweifenden Lebensstil zurück und schmeißt wilde Partys, die bald ausarten. Doch dann steht Alan plötzlich wieder vor der Tür.
225 | Sitcom
226 | 3.0.
227 | S4 E1
228 | 2006-09-18
229 | ...
230 |
231 | ```
232 |
233 | ---
234 | ```yaml
235 | Local Images Cache: false
236 | ```
237 | **true**: Download the images from SD in a local folder. This option atuomatically enables the server so clients can access to the images.
238 | **false**: images are not downloaded locally
239 |
240 | ---
241 |
242 | ```yaml
243 | Hostname: localhost:8080
244 | ```
245 | **Hostname:** hostname + port of the local server for the images
246 | ---
247 |
248 | ```yaml
249 | Images Path: /data/images
250 | ```
251 |
252 | Path to cache images locally. Only useful if Local Images Cache = true
253 |
254 | ---
255 |
256 | ```yaml
257 | Proxy Images: false
258 | ```
259 |
260 | **True**: (Overrides local image cache option) Instead of downloading the images locally, it will act as a reverse proxy between the clients and guide2go server.
261 | This is only usefull when there are not too many clients on your network, not too many channels on your EPG and you are not downloading more than 1-3 days of EPG data.
262 | It is very usefull if you dont want to bother setting up a cache folder for your EPG images.
263 |
264 | ---
265 |
266 | ```yaml
267 | Insert credits tag into XML file: false
268 | ```
269 | **true:** Adds the credits (director, actor, producer, writer) to the program information, if available.
270 | ```xml
271 |
272 |
273 | Two and a Half Men
274 | Ich arbeite für Caligula
275 | ...
276 |
277 | Jamie Widdoes
278 | Charlie Sheen
279 | Jon Cryer
280 | Angus T. Jones
281 | Marin Hinkle
282 | Holland Taylor
283 | Melanie Lynskey
284 | Chuck Lorre
285 | Lee Aronsohn
286 | Susan Beavers
287 | Don Foster
288 |
289 | ...
290 |
291 | ```
292 |
293 | ---
294 |
295 | ```yaml
296 | Rating:
297 | Insert rating tag into XML file: true
298 | ...
299 | ```
300 | **true:** Adds the TV parental guidelines to the program information.
301 |
302 | ```xml
303 |
304 |
305 | Two and a Half Men
306 | Ich arbeite für Caligula
307 | de
308 | ...
309 |
310 | 12
311 |
312 | ...
313 |
314 | ```
315 | **false:** TV parental guidelines are not used. Further rating settings are ignored.
316 | ```xml
317 |
318 |
319 | Two and a Half Men
320 | Ich arbeite für Caligula
321 | de
322 | ...
323 |
324 | ```
325 |
326 | ```yaml
327 | Rating:
328 | ...
329 | Maximum rating entries. 0 for all entries: 1
330 | ...
331 | ```
332 | Specifies the number of maximum rating entries. If the value is 0, all parental guidelines available from Schedules Direct are used. Depending on the preferred countries.
333 |
334 | ```yaml
335 | Rating:
336 | ...
337 | Preferred countries. ISO 3166-1 alpha-3 country code. Leave empty for all systems:
338 | - DEU
339 | - CHE
340 | - USA
341 | ...
342 | ```
343 | Sets the order of the preferred countries [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3 "ISO 3166-1 alpha-3").
344 | Parental guidelines are not available for every country and program information. Trial and error.
345 | If no country is specified, all available countries are used. Many clients ignore a list with more than one entry or use the first entry.
346 |
347 | **If no country is specified:**
348 | If a rating entry exists in the same language as the Schedules Direct Lineup, it will be set to the top. In this example German (DEU).
349 |
350 | Lineup: **DEU**-1000097-DEFAULT
351 | 1st rating system (Germany): Freiwillige Selbstkontrolle der Filmwirtschaft
352 | ```xml
353 | ...
354 |
355 | 12
356 |
357 |
358 | TV14
359 |
360 | ...
361 | ```
362 |
363 | ```yaml
364 | Rating:
365 | ...
366 | Use country code as rating system: false
367 | ```
368 |
369 | **true:**
370 | ```xml
371 |
372 | 12
373 |
374 |
375 | TV14
376 |
377 |
378 | ```
379 |
380 | **false:**
381 | ```xml
382 |
383 | 12
384 |
385 |
386 | TV14
387 |
388 | ```
389 |
390 | ---
391 |
392 | ```
393 | Show download errors from Schedules Direct in the log: false
394 | ```
395 | **true:** Shows incorrect downloads of Schedules Direct in the log.
396 |
397 | Example:
398 | ```
399 | 2020/07/18 19:10:53 [ERROR] Could not find requested image. Post message to http://forums.schedulesdirect.org/viewforum.php?f=6 if you are having issues. [SD API Error Code: 5000] Program ID: EP03481925
400 | ```
401 |
402 | ### Create the XMLTV file using the command line (CLI):
403 |
404 | ```
405 | guide2go -config MY_CONFIG_FILE.yaml
406 | ```
407 | **The configuration file must have already been created.**
408 |
--------------------------------------------------------------------------------
/cache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | "os"
11 | "strconv"
12 | "sync"
13 | )
14 |
15 | // Cache : Cache file
16 | var Cache cache
17 | var ImageError bool = false
18 |
19 | // Init : Inti cache
20 | func (c *cache) Init() {
21 |
22 | if c.Schedule == nil {
23 | c.Schedule = make(map[string][]G2GCache)
24 | }
25 |
26 | c.Channel = make(map[string]G2GCache)
27 |
28 | if c.Program == nil {
29 | c.Program = make(map[string]G2GCache)
30 | }
31 |
32 | if c.Metadata == nil {
33 | c.Metadata = make(map[string]G2GCache)
34 | }
35 |
36 | }
37 |
38 | func (c *cache) Remove() {
39 |
40 | if len(Config.Files.Cache) != 0 {
41 |
42 | showInfo("G2G", fmt.Sprintf("%s [%s]", getMsg(0301), Config.Files.Cache))
43 | os.RemoveAll(Config.Files.Cache)
44 |
45 | c.Init()
46 |
47 | }
48 |
49 | }
50 |
51 | func (c *cache) AddStations(data *[]byte, lineup string) {
52 |
53 | c.Lock()
54 | defer c.Unlock()
55 |
56 | var g2gCache G2GCache
57 | var sdData SDStation
58 |
59 | err := json.Unmarshal(*data, &sdData)
60 | if err != nil {
61 | ShowErr(err)
62 | return
63 | }
64 |
65 | var channelIDs = Config.GetChannelList(lineup)
66 |
67 | for _, sd := range sdData.Stations {
68 |
69 | if ContainsString(channelIDs, sd.StationID) != -1 {
70 |
71 | g2gCache.StationID = sd.StationID
72 | g2gCache.Name = sd.Name
73 | g2gCache.Callsign = sd.Callsign
74 | g2gCache.Affiliate = sd.Affiliate
75 | g2gCache.BroadcastLanguage = sd.BroadcastLanguage
76 | g2gCache.Logo = sd.Logo
77 |
78 | c.Channel[sd.StationID] = g2gCache
79 |
80 | }
81 |
82 | }
83 |
84 | }
85 |
86 | func (c *cache) AddSchedule(data *[]byte) {
87 |
88 | c.Lock()
89 | defer c.Unlock()
90 |
91 | var g2gCache G2GCache
92 | var sdData []SDSchedule
93 |
94 | err := json.Unmarshal(*data, &sdData)
95 | if err != nil {
96 | ShowErr(err)
97 | return
98 | }
99 |
100 | for _, sd := range sdData {
101 |
102 | if _, ok := c.Schedule[sd.StationID]; !ok {
103 | c.Schedule[sd.StationID] = []G2GCache{}
104 | }
105 |
106 | for _, p := range sd.Programs {
107 |
108 | g2gCache.AirDateTime = p.AirDateTime
109 | g2gCache.AudioProperties = p.AudioProperties
110 | g2gCache.Duration = p.Duration
111 | g2gCache.LiveTapeDelay = p.LiveTapeDelay
112 | g2gCache.New = p.New
113 | g2gCache.Md5 = p.Md5
114 | g2gCache.ProgramID = p.ProgramID
115 | g2gCache.Ratings = p.Ratings
116 | g2gCache.VideoProperties = p.VideoProperties
117 |
118 | c.Schedule[sd.StationID] = append(c.Schedule[sd.StationID], g2gCache)
119 |
120 | }
121 |
122 | }
123 |
124 | }
125 |
126 | func (c *cache) AddProgram(gzip *[]byte, wg *sync.WaitGroup) {
127 |
128 | c.Lock()
129 |
130 | defer func() {
131 | c.Unlock()
132 | wg.Done()
133 | }()
134 |
135 | b, err := gUnzip(*gzip)
136 | if err != nil {
137 | ShowErr(err)
138 | return
139 | }
140 |
141 | var g2gCache G2GCache
142 | var sdData []SDProgram
143 |
144 | err = json.Unmarshal(b, &sdData)
145 | if err != nil {
146 | ShowErr(err)
147 | return
148 | }
149 |
150 | for _, sd := range sdData {
151 |
152 | g2gCache.Descriptions = sd.Descriptions
153 | g2gCache.EpisodeTitle150 = sd.EpisodeTitle150
154 | g2gCache.Genres = sd.Genres
155 |
156 | g2gCache.HasEpisodeArtwork = sd.HasEpisodeArtwork
157 | g2gCache.HasImageArtwork = sd.HasImageArtwork
158 | g2gCache.HasSeriesArtwork = sd.HasSeriesArtwork
159 | g2gCache.Metadata = sd.Metadata
160 | g2gCache.OriginalAirDate = sd.OriginalAirDate
161 | g2gCache.ResourceID = sd.ResourceID
162 | g2gCache.ShowType = sd.ShowType
163 | g2gCache.Titles = sd.Titles
164 | g2gCache.ContentRating = sd.ContentRating
165 | g2gCache.Cast = sd.Cast
166 | g2gCache.Crew = sd.Crew
167 |
168 | c.Program[sd.ProgramID] = g2gCache
169 |
170 | }
171 |
172 | }
173 |
174 | func (c *cache) AddMetadata(gzip *[]byte, wg *sync.WaitGroup) {
175 |
176 | c.Lock()
177 | defer func() {
178 | c.Unlock()
179 | wg.Done()
180 | }()
181 |
182 | b, err := gUnzip(*gzip)
183 | if err != nil {
184 | ShowErr(err)
185 | return
186 | }
187 |
188 | var tmp = make([]interface{}, 0)
189 |
190 | var g2gCache G2GCache
191 |
192 | err = json.Unmarshal(b, &tmp)
193 | if err != nil {
194 | ShowErr(err)
195 | return
196 | }
197 |
198 | for _, t := range tmp {
199 |
200 | var sdData SDMetadata
201 |
202 | jsonByte, _ := json.Marshal(t)
203 | err = json.Unmarshal(jsonByte, &sdData)
204 | if err != nil {
205 |
206 | var sdError SDError
207 | err = json.Unmarshal(jsonByte, &sdError)
208 | if err == nil {
209 |
210 | if Config.Options.SDDownloadErrors {
211 | err = fmt.Errorf("%s [SD API Error Code: %d] Program ID: %s", sdError.Data.Message, sdError.Data.Code, sdError.ProgramID)
212 | ShowErr(err)
213 | }
214 |
215 | }
216 |
217 | } else {
218 |
219 | g2gCache.Data = sdData.Data
220 | c.Metadata[sdData.ProgramID] = g2gCache
221 |
222 | }
223 |
224 | }
225 | }
226 |
227 | func (c *cache) GetAllProgramIDs() (programIDs []string) {
228 |
229 | for _, channel := range c.Schedule {
230 |
231 | for _, schedule := range channel {
232 |
233 | if ContainsString(programIDs, schedule.ProgramID) == -1 {
234 | programIDs = append(programIDs, schedule.ProgramID)
235 | }
236 |
237 | }
238 |
239 | }
240 |
241 | return
242 | }
243 |
244 | func (c *cache) GetRequiredProgramIDs() (programIDs []string) {
245 |
246 | var allProgramIDs = c.GetAllProgramIDs()
247 |
248 | for _, id := range allProgramIDs {
249 |
250 | if _, ok := c.Program[id]; !ok {
251 |
252 | if ContainsString(programIDs, id) == -1 {
253 | programIDs = append(programIDs, id)
254 | }
255 |
256 | }
257 |
258 | }
259 |
260 | return
261 | }
262 |
263 | func (c *cache) GetRequiredMetaIDs() (metaIDs []string) {
264 |
265 | for id := range c.Program {
266 |
267 | if len(id) > 10 {
268 |
269 | if _, ok := c.Metadata[id[:10]]; !ok {
270 | metaIDs = append(metaIDs, id[:10])
271 | }
272 |
273 | }
274 | }
275 |
276 | return
277 | }
278 |
279 | func (c *cache) Open() (err error) {
280 |
281 | data, err := ioutil.ReadFile(Config.Files.Cache)
282 |
283 | if err != nil {
284 | c.Init()
285 | c.Save()
286 | return nil
287 | }
288 |
289 | // Open config file and convert Yaml to Struct (config)
290 | err = json.Unmarshal(data, &c)
291 | if err != nil {
292 | return
293 | }
294 |
295 | return
296 | }
297 |
298 | func (c *cache) Save() (err error) {
299 |
300 | c.Lock()
301 | defer c.Unlock()
302 |
303 | data, err := json.MarshalIndent(&c, "", " ")
304 | if err != nil {
305 | return err
306 | }
307 |
308 | err = ioutil.WriteFile(Config.Files.Cache, data, 0644)
309 | if err != nil {
310 | return
311 | }
312 |
313 | return
314 | }
315 |
316 | func (c *cache) CleanUp() {
317 |
318 | var count int
319 | showInfo("G2G", fmt.Sprintf("Clean up Cache [%s]", Config.Files.Cache))
320 |
321 | var programIDs = c.GetAllProgramIDs()
322 |
323 | for id := range c.Program {
324 |
325 | if ContainsString(programIDs, id) == -1 {
326 |
327 | count++
328 | delete(c.Program, id)
329 | delete(c.Metadata, id[0:10])
330 |
331 | }
332 |
333 | }
334 |
335 | c.Channel = make(map[string]G2GCache)
336 | c.Schedule = make(map[string][]G2GCache)
337 |
338 | showInfo("G2G", fmt.Sprintf("Deleted Program Informations: %d", count))
339 |
340 | err := c.Save()
341 | if err != nil {
342 | ShowErr(err)
343 | return
344 | }
345 | }
346 |
347 | // Get data from cache
348 | func (c *cache) GetTitle(id, lang string) (t []Title) {
349 |
350 | if p, ok := c.Program[id]; ok {
351 |
352 | var title Title
353 |
354 | for _, s := range p.Titles {
355 | title.Value = s.Title120
356 | title.Lang = lang
357 | t = append(t, title)
358 | }
359 |
360 | }
361 |
362 | if len(t) == 0 {
363 | var title Title
364 | title.Value = "No EPG Info"
365 | title.Lang = "en"
366 | t = append(t, title)
367 | }
368 |
369 | return
370 | }
371 |
372 | func (c *cache) GetSubTitle(id, lang string) (s SubTitle) {
373 |
374 | if p, ok := c.Program[id]; ok {
375 |
376 | if len(p.EpisodeTitle150) != 0 {
377 |
378 | s.Value = p.EpisodeTitle150
379 | s.Lang = lang
380 |
381 | } else {
382 |
383 | for _, d := range p.Descriptions.Description100 {
384 |
385 | s.Value = d.Description
386 | s.Lang = d.DescriptionLanguage
387 |
388 | }
389 |
390 | }
391 |
392 | }
393 |
394 | return
395 | }
396 |
397 | func (c *cache) GetDescs(id, subTitle string) (de []Desc) {
398 |
399 | if p, ok := c.Program[id]; ok {
400 |
401 | d := p.Descriptions
402 |
403 | var desc Desc
404 |
405 | for _, tmp := range d.Description1000 {
406 |
407 | switch Config.Options.SubtitleIntoDescription {
408 |
409 | case true:
410 | if len(subTitle) != 0 {
411 | desc.Value = fmt.Sprintf("[%s]\n%s", subTitle, tmp.Description)
412 | break
413 | }
414 |
415 | fallthrough
416 | case false:
417 | desc.Value = tmp.Description
418 |
419 | }
420 |
421 | desc.Lang = tmp.DescriptionLanguage
422 |
423 | de = append(de, desc)
424 | }
425 |
426 | }
427 |
428 | return
429 | }
430 |
431 | func (c *cache) GetCredits(id string) (cr Credits) {
432 |
433 | if Config.Options.Credits {
434 |
435 | if p, ok := c.Program[id]; ok {
436 |
437 | // Crew
438 | for _, crew := range p.Crew {
439 |
440 | switch crew.Role {
441 |
442 | case "Director":
443 | cr.Director = append(cr.Director, Director{Value: crew.Name})
444 |
445 | case "Producer":
446 | cr.Producer = append(cr.Producer, Producer{Value: crew.Name})
447 |
448 | case "Presenter":
449 | cr.Presenter = append(cr.Presenter, Presenter{Value: crew.Name})
450 |
451 | case "Writer":
452 | cr.Writer = append(cr.Writer, Writer{Value: crew.Name})
453 |
454 | }
455 |
456 | }
457 |
458 | // Cast
459 | for _, cast := range p.Cast {
460 |
461 | switch cast.Role {
462 |
463 | case "Actor":
464 | cr.Actor = append(cr.Actor, Actor{Value: cast.Name, Role: cast.CharacterName})
465 |
466 | }
467 |
468 | }
469 |
470 | }
471 |
472 | }
473 |
474 | return
475 | }
476 |
477 | func (c *cache) GetCategory(id string) (ca []Category) {
478 |
479 | if p, ok := c.Program[id]; ok {
480 |
481 | for _, g := range p.Genres {
482 |
483 | var category Category
484 | category.Value = g
485 | category.Lang = "en"
486 |
487 | ca = append(ca, category)
488 |
489 | }
490 |
491 | }
492 |
493 | return
494 | }
495 |
496 | func (c *cache) GetEpisodeNum(id string) (ep []EpisodeNum) {
497 |
498 | var seaseon, episode int
499 |
500 | if p, ok := c.Program[id]; ok {
501 |
502 | for _, m := range p.Metadata {
503 |
504 | seaseon = m.Gracenote.Season
505 | episode = m.Gracenote.Episode
506 |
507 | var episodeNum EpisodeNum
508 |
509 | if seaseon != 0 && episode != 0 {
510 |
511 | episodeNum.Value = fmt.Sprintf("%d.%d.", seaseon-1, episode-1)
512 | episodeNum.System = "xmltv_ns"
513 |
514 | ep = append(ep, episodeNum)
515 | }
516 |
517 | }
518 |
519 | if seaseon != 0 && episode != 0 {
520 |
521 | var episodeNum EpisodeNum
522 | episodeNum.Value = fmt.Sprintf("S%d E%d", seaseon, episode)
523 | episodeNum.System = "onscreen"
524 | ep = append(ep, episodeNum)
525 |
526 | }
527 |
528 | if len(ep) == 0 {
529 |
530 | var episodeNum EpisodeNum
531 |
532 | switch id[0:2] {
533 |
534 | case "EP":
535 | episodeNum.Value = id[0:10] + "." + id[10:]
536 |
537 | case "SH", "MV":
538 | episodeNum.Value = id[0:10] + ".0000"
539 |
540 | default:
541 | episodeNum.Value = id
542 | }
543 |
544 | episodeNum.System = "dd_progid"
545 |
546 | ep = append(ep, episodeNum)
547 |
548 | }
549 |
550 | if len(p.OriginalAirDate) > 0 {
551 |
552 | var episodeNum EpisodeNum
553 | episodeNum.Value = p.OriginalAirDate
554 | episodeNum.System = "original-air-date"
555 | ep = append(ep, episodeNum)
556 |
557 | }
558 |
559 | }
560 |
561 | return
562 | }
563 |
564 | func (c *cache) GetPreviouslyShown(id string) (prev *PreviouslyShown) {
565 |
566 | prev = &PreviouslyShown{}
567 |
568 | if p, ok := c.Program[id]; ok {
569 | prev.Start = p.OriginalAirDate
570 | }
571 |
572 | return
573 | }
574 |
575 | func GetImageUrl(urlid string, token string, name string) {
576 | url := urlid + "?token=" + token
577 | filename := Config.Options.ImagesPath + name
578 | if a, err := os.Stat(filename); err != nil || a.Size() < 500 {
579 | file, _ := os.Create(filename)
580 | defer file.Close()
581 | req, _ := http.Get(url)
582 | defer req.Body.Close()
583 | io.Copy(file, req.Body)
584 | info, _ := os.Stat(filename)
585 | if info.Size() < 500 {
586 | log.Println("Max image limit downloaded --skipping image download")
587 | ImageError = true
588 | return
589 | }
590 |
591 | }
592 | }
593 |
594 | func (c *cache) GetIcon(id string) (i []Icon) {
595 |
596 | var aspects = []string{"2x3", "4x3", "3x4", "16x9"}
597 | var uri string
598 | var width, height int
599 | var err error
600 | var nameFinal string
601 | switch Config.Options.PosterAspect {
602 |
603 | case "all":
604 | break
605 |
606 | default:
607 | aspects = []string{Config.Options.PosterAspect}
608 |
609 | }
610 |
611 | if m, ok := c.Metadata[id]; ok {
612 | var nameTemp string
613 | for _, aspect := range aspects {
614 | var maxWidth, maxHeight int
615 | var finalCategory string = ""
616 | for _, icon := range m.Data {
617 | if finalCategory == "" && (icon.Category == "Poster Art" || icon.Category == "Box Art" || icon.Category == "Banner-L1" || icon.Category == "Banner-L2") {
618 | finalCategory = icon.Category
619 | } else if finalCategory == "" && icon.Category == "VOD Art" {
620 | finalCategory = icon.Category
621 | }
622 | if icon.Category != finalCategory {
623 | continue
624 | }
625 |
626 | if icon.URI[0:7] != "http://" && icon.URI[0:8] != "https://" {
627 | nameTemp = icon.URI
628 | icon.URI = fmt.Sprintf("https://json.schedulesdirect.org/20141201/image/%s", icon.URI)
629 | }
630 |
631 | if icon.Aspect == aspect {
632 |
633 | width, err = strconv.Atoi(icon.Width)
634 | if err != nil {
635 | return
636 | }
637 |
638 | height, err = strconv.Atoi(icon.Height)
639 | if err != nil {
640 | return
641 | }
642 |
643 | if width > maxWidth {
644 | maxWidth = width
645 | maxHeight = height
646 | uri = icon.URI
647 | nameFinal = nameTemp
648 | }
649 |
650 | }
651 |
652 | }
653 |
654 | if maxWidth > 0 {
655 | if Config.Options.TVShowImages && !ImageError {
656 | GetImageUrl(uri, Token, nameFinal)
657 | }
658 | path := "http://" + Config.Options.Hostname + "/images/" + nameFinal
659 | i = append(i, Icon{Src: path, Height: maxHeight, Width: maxWidth})
660 | }
661 |
662 | }
663 |
664 | }
665 |
666 | return
667 | }
668 |
669 | func (c *cache) GetRating(id, countryCode string) (ra []Rating) {
670 |
671 | if !Config.Options.Rating.Guidelines {
672 | return
673 | }
674 |
675 | var add = func(code, body, country string) {
676 |
677 | switch Config.Options.Rating.CountryCodeAsSystem {
678 |
679 | case true:
680 | ra = append(ra, Rating{Value: code, System: country})
681 |
682 | case false:
683 | ra = append(ra, Rating{Value: code, System: body})
684 |
685 | }
686 |
687 | }
688 |
689 | /*
690 | var prepend = func(code, body, country string) {
691 |
692 | switch Config.Options.Rating.CountryCodeAsSystem {
693 |
694 | case true:
695 | ra = append([]Rating{{Value: code, System: country}}, ra...)
696 |
697 | case false:
698 | ra = append([]Rating{{Value: code, System: body}}, ra...)
699 |
700 | }
701 |
702 | }
703 | */
704 |
705 | if p, ok := c.Program[id]; ok {
706 |
707 | switch len(Config.Options.Rating.Countries) {
708 |
709 | case 0:
710 | for _, r := range p.ContentRating {
711 |
712 | if len(ra) == Config.Options.Rating.MaxEntries && Config.Options.Rating.MaxEntries != 0 {
713 | return
714 | }
715 |
716 | if countryCode == r.Country {
717 | add(r.Code, r.Body, r.Country)
718 | }
719 |
720 | }
721 |
722 | for _, r := range p.ContentRating {
723 |
724 | if len(ra) == Config.Options.Rating.MaxEntries && Config.Options.Rating.MaxEntries != 0 {
725 | return
726 | }
727 |
728 | if countryCode != r.Country {
729 | add(r.Code, r.Body, r.Country)
730 | }
731 |
732 | }
733 |
734 | default:
735 | for _, cCode := range Config.Options.Rating.Countries {
736 |
737 | for _, r := range p.ContentRating {
738 |
739 | if len(ra) == Config.Options.Rating.MaxEntries && Config.Options.Rating.MaxEntries != 0 {
740 | return
741 | }
742 |
743 | if cCode == r.Country {
744 |
745 | add(r.Code, r.Body, r.Country)
746 |
747 | }
748 |
749 | }
750 |
751 | }
752 |
753 | }
754 |
755 | }
756 |
757 | return
758 | }
759 |
--------------------------------------------------------------------------------