├── 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 | --------------------------------------------------------------------------------