diff --git a/.env.sample b/.env.sample index 75aca31..6cab794 100644 --- a/.env.sample +++ b/.env.sample @@ -3,5 +3,7 @@ MIRAKURUN_PORT=40772 MIRAKURUN_HTTPS=false CHINACHU_IP=localhost CHINACHU_PORT=10772 +LAPIS_HOSTNAME=localhost +FFMPEG_BIN=ffmpeg LAPIS_PORT=8080 GIN_MODE=release \ No newline at end of file diff --git a/.gitignore b/.gitignore index af8dfcc..1747157 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ .env lapis +lapis-v2 main vendor vendor/* +subs/* diff --git a/Dockerfile b/Dockerfile index e200f5a..28332a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ # lapis builder image -FROM golang:latest as builder +FROM golang:1.13.4 as builder LABEL maintainer "plainbanana " ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 -WORKDIR /go/src/github.com/plainbanana/lapis +WORKDIR /plainbanana/lapis COPY . . RUN make @@ -14,5 +14,5 @@ FROM alpine LABEL maintainer "plainbanana " ENV DOTENV=false RUN apk add --no-cache ca-certificates -COPY --from=builder /go/src/github.com/plainbanana/lapis/lapis /lapis +COPY --from=builder /plainbanana/lapis/lapis /lapis CMD ["/lapis"] \ No newline at end of file diff --git a/app/chinachu.go b/app/chinachu.go index 02b8ed8..cb04be6 100644 --- a/app/chinachu.go +++ b/app/chinachu.go @@ -15,6 +15,15 @@ import ( "github.com/plainbanana/lapis/entities" ) +// Xmldateformat is xmltv +const Xmldateformat = "20060102150400 -0700" + +// Originalairdateformat chinachu format +const Originalairdateformat = "2006-01-02 15:04:05" + +// Episodedateformat episodedate +const Episodedateformat = "0102" + func hashMod(value string) int { str := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) numHash, err := strconv.ParseInt(str[:10], 16, 64) @@ -32,9 +41,6 @@ func hashMod(value string) int { // ConvertEpgToXML : GET /api/schedule/programs.json -> epg.xml func ConvertEpgToXML() *entities.Guide { - const xmldateformat = "20060102150400 -0700" - const originalairdateformat = "2006-01-02 15:04:05" - const episodedateformat = "0102" base := "http://" + os.Getenv("CHINACHU_IP") + ":" + os.Getenv("CHINACHU_PORT") + "/api/schedule/programs.json" if os.Getenv("MIRAKURUN_HTTPS") == "true" { @@ -61,15 +67,16 @@ func ConvertEpgToXML() *entities.Guide { c.ID = v.GuideNumber c.DisplayName.DisplayName = v.GuideName c.DisplayName.Lang = "ja_JP" + c.Icon.Src = setChannelIconURL(c.ID) xml.Channel = append(xml.Channel, c) } for _, v := range s { var p entities.ProgrammeGuide - p.Start = time.Unix(0, v.Start*1000000).Format(xmldateformat) - p.Stop = time.Unix(0, v.End*1000000).Format(xmldateformat) + p.Start = time.Unix(0, v.Start*1000000).Format(Xmldateformat) + p.Stop = time.Unix(0, v.End*1000000).Format(Xmldateformat) p.Channel = strconv.Itoa(v.Channel.SID) p.Category.Category = v.Category - p.Desc.Desc = v.Detail + p.Desc.Desc = v.Detail + " lapisID:" + v.ID titleSlice := strings.Split(v.Title, "▽") p.Title.Title = titleSlice[0] if len(titleSlice) >= 2 { @@ -83,7 +90,7 @@ func ConvertEpgToXML() *entities.Guide { p.EpisodeNum.System = "dd_progid" var tail string if v.Episode == 0 { - tail = time.Unix(0, v.Start*1000000).Format(episodedateformat) + tail = time.Unix(0, v.Start*1000000).Format(Episodedateformat) } else { tail = fmt.Sprintf("%04d", v.Episode) } diff --git a/app/mirakurun.go b/app/mirakurun.go index f358954..019902d 100644 --- a/app/mirakurun.go +++ b/app/mirakurun.go @@ -1,7 +1,9 @@ package app import ( + "encoding/base64" "encoding/json" + "fmt" "log" "net/http" "os" @@ -10,13 +12,12 @@ import ( "github.com/plainbanana/lapis/entities" ) +// MirakurunBase is baseurl +var MirakurunBase string + // SetLineup : get ALL channel func SetLineup() []entities.Lineup { - base := "http://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + "/api/channels/" - if os.Getenv("MIRAKURUN_HTTPS") == "true" { - base = "https://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + "/api/channels/" - } - + base := MirakurunBase + "/api/channels/" res, err := http.Get(base) // get channel lists as json if err != nil { log.Println(err) @@ -30,16 +31,50 @@ func SetLineup() []entities.Lineup { log.Println(err) } + lapisuri := os.Getenv("LAPIS_HOSTNAME") + if lapisuri == "" { + lapisuri = "localhost" + } + var lineups []entities.Lineup for _, v := range s { for _, vv := range v.Services { var items entities.Lineup items.GuideName = vv.Name items.GuideNumber = strconv.Itoa(vv.ServiceID) - items.URL = base + v.Type + "/" + v.Channel + "/services/" + strconv.Itoa(vv.ServiceID) + "/stream/" + origURL := base + v.Type + "/" + v.Channel + "/services/" + strconv.Itoa(vv.ServiceID) + "/stream/" + b64url := base64.StdEncoding.EncodeToString([]byte(origURL)) + items.URL = "http://" + lapisuri + ":" + entities.LapisPort + "/stream/" + b64url lineups = append(lineups, items) } } return lineups } + +// setChannelIconURL set channnel png logo url +func setChannelIconURL(serviceid string) string { + id := "" + + base := MirakurunBase + "/api/services/" + res, err := http.Get(base) + if err != nil { + log.Println(err) + } + defer res.Body.Close() + + var s entities.Service + decoder := json.NewDecoder(res.Body) + err = decoder.Decode(&s) + if err != nil { + log.Println(err) + } + + for _, v := range s { + if fmt.Sprint(v.ServiceID) == serviceid { + id = fmt.Sprint(v.ID) + } + } + + return MirakurunBase + "/api/services/" + id + "/logo" +} diff --git a/entities/constructor.go b/entities/constructor.go index cdc67c6..6db9f6c 100644 --- a/entities/constructor.go +++ b/entities/constructor.go @@ -5,6 +5,7 @@ func NewDevice() Device { dev := Device{} dev.FriendlyName = "lapis" dev.Manufacturer = "Silicondust" + // Built in Transcoder dev.ModelNumber = "HDTC-2US" dev.FirmwareName = "hdhomeruntc_atsc" dev.DeviceID = "12345678" diff --git a/entities/entities.go b/entities/entities.go index 0acc967..ae319a8 100644 --- a/entities/entities.go +++ b/entities/entities.go @@ -2,6 +2,12 @@ package entities import "encoding/xml" +// LapisVersion is version +var LapisVersion = "1.1.0" + +// LapisPort is port of lapis +var LapisPort string + // Device : device info type Device struct { FriendlyName string `json:"FriendlyName"` @@ -48,19 +54,32 @@ type DeviceChild struct { } // Service : json -type Service struct { - ID int `json:"id"` - ServiceID int `json:"serviceId"` - NetworkID int `json:"networkId"` - Name string `json:"name"` +type Service []struct { + ID int64 `json:"id"` + ServiceID int `json:"serviceId"` + NetworkID int `json:"networkId"` + Name string `json:"name"` + Type int `json:"type"` + LogoID int `json:"logoId"` + RemoteControlKeyID int `json:"remoteControlKeyId"` + Channel struct { + Type string `json:"type"` + Channel string `json:"channel"` + } `json:"channel"` + HasLogoData bool `json:"hasLogoData"` } // Channel : json type Channel struct { - Type string `json:"type"` - Channel string `json:"channel"` - Name string `json:"name"` - Services []Service `json:"services"` + Type string `json:"type"` + Channel string `json:"channel"` + Name string `json:"name"` + Services []struct { + ID int `json:"id"` + ServiceID int `json:"serviceId"` + NetworkID int `json:"networkId"` + Name string `json:"name"` + } `json:"services"` } // Channels : json @@ -76,14 +95,14 @@ type Guide struct { // ChannelGuide : xml type ChannelGuide struct { - ID string `xml:"id,attr"` - DisplayName DisplayNameGuide `xml:"display-name"` -} - -// DisplayNameGuide : xml -type DisplayNameGuide struct { - Lang string `xml:"lang,attr"` - DisplayName string `xml:",chardata"` + ID string `xml:"id,attr"` + DisplayName struct { + Lang string `xml:"lang,attr"` + DisplayName string `xml:",chardata"` + } `xml:"display-name"` + Icon struct { + Src string `xml:"src,attr"` + } `xml:"icon"` } // ProgrammeGuide : xml @@ -154,3 +173,44 @@ type ChannelSchedule struct { SID int `json:"sid"` Name string `json:"name"` } + +// MirakurunPrograms : json +type MirakurunPrograms []struct { + ID int64 `json:"id"` + EventID int `json:"eventId"` + ServiceID int `json:"serviceId"` + NetworkID int `json:"networkId"` + StartAt int64 `json:"startAt"` + Duration int `json:"duration"` + IsFree bool `json:"isFree"` + Name string `json:"name"` + Description string `json:"description"` + Video struct { + Type string `json:"type"` + Resolution string `json:"resolution"` + StreamContent int `json:"streamContent"` + ComponentType int `json:"componentType"` + } `json:"video"` + Audio struct { + SamplingRate int `json:"samplingRate"` + ComponentType int `json:"componentType"` + } `json:"audio"` + Genres []struct { + Lv1 int `json:"lv1"` + Lv2 int `json:"lv2"` + Un1 int `json:"un1"` + Un2 int `json:"un2"` + } `json:"genres,omitempty"` + RelatedItems []struct { + ServiceID int `json:"serviceId"` + EventID int `json:"eventId"` + } `json:"relatedItems,omitempty"` + Extended struct { + ProgrammeContent string `json:"番組内容"` + Performer string `json:"出演者"` + OriginalScreenplay string `json:"原作・脚本"` + Director string `json:"監督・演出"` + Production string `json:"制作"` + Music string `json:"音楽"` + } `json:"extended,omitempty"` +} diff --git a/go.mod b/go.mod index 29f846c..7baaab3 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,16 @@ module github.com/plainbanana/lapis go 1.13.4 require ( - github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect - github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 - github.com/golang/protobuf v1.1.0 // indirect + github.com/djherbis/stream v1.2.0 + github.com/gin-gonic/gin v1.5.0 + github.com/go-playground/universal-translator v0.17.0 // indirect github.com/joho/godotenv v1.2.0 - github.com/mattn/go-isatty v0.0.3 // indirect - github.com/stretchr/testify v1.4.0 // indirect - github.com/ugorji/go v1.1.1 // indirect - golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 // indirect - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect - gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + github.com/json-iterator/go v1.1.8 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab // indirect + gopkg.in/go-playground/validator.v9 v9.30.2 // indirect + gopkg.in/yaml.v2 v2.2.7 // indirect ) diff --git a/go.sum b/go.sum index 9f2f3c1..2b45094 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,72 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 h1:cZPJWzd2oNeoS0oJM2TlN9rl0OnCgUr10gC8Q4mH+6M= -github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= -github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/stream v1.2.0 h1:6KwMyps3yTU58GFfrHJwhuVfn+GU2MyajuLcncJcYCo= +github.com/djherbis/stream v1.2.0/go.mod h1:ZNVKPVRCmrwhCwQHZUpVHHrq2rtGLrG1t3T/TThYLP8= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc= github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w= -github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 h1:Dd5RoEW+yQi+9DMybroBctIdyiwuNT7sJFMC27/6KxI= -golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM= +golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/go-playground/validator.v9 v9.30.2 h1:icxYLlYflpazIV3ufMoNB9h9SYMQ37DZ8CTwkU4pnOs= +gopkg.in/go-playground/validator.v9 v9.30.2/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 0ea3a08..d995f16 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,22 @@ import ( "github.com/gin-gonic/gin" "github.com/joho/godotenv" + "github.com/plainbanana/lapis/app" + "github.com/plainbanana/lapis/entities" "github.com/plainbanana/lapis/router" ) +func init() { + if os.Getenv("DOTENV") == "true" { + envLoad() + } + + app.MirakurunBase = "http://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + if os.Getenv("MIRAKURUN_HTTPS") == "true" { + app.MirakurunBase = "https://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + } +} + func envLoad() { err := godotenv.Load() if err != nil { @@ -17,10 +30,6 @@ func envLoad() { } func main() { - if os.Getenv("DOTENV") == "true" { - envLoad() - } - r := gin.Default() r.GET("/discover.json", router.Discover) @@ -31,6 +40,12 @@ func main() { r.GET("/ContentDirectory.xml", router.ContentDirectory) r.GET("/epg.xml", router.EPG) r.POST("/lineup.post", router.PostLineup) + r.GET("/stream/:OriginURL", router.Stream) + + entities.LapisPort = os.Getenv("LAPIS_PORT") + if entities.LapisPort == "" { + entities.LapisPort = "8080" + } - r.Run(":" + os.Getenv("LAPIS_PORT")) + r.Run(":" + entities.LapisPort) } diff --git a/router/homerun.go b/router/homerun.go index 03e4db1..7fb3b6d 100644 --- a/router/homerun.go +++ b/router/homerun.go @@ -9,7 +9,8 @@ import ( // Discover : GET /discover.json func Discover(c *gin.Context) { device := entities.NewDevice() - c.BindJSON(&device) + // does not have built in Transcoder + device.ModelNumber = "HDHR5-2US" c.JSON(200, device) } diff --git a/router/lapis.go b/router/lapis.go new file mode 100644 index 0000000..3f81d90 --- /dev/null +++ b/router/lapis.go @@ -0,0 +1,403 @@ +package router + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path" + "strings" + "syscall" + "time" + + "github.com/djherbis/stream" + "github.com/gin-gonic/gin" + "github.com/plainbanana/lapis/app" + "github.com/plainbanana/lapis/entities" +) + +var jst = time.FixedZone("Asia/Tokyo", 9*60*60) + +// Stream is ffmpeg streamer +func Stream(c *gin.Context) { + b64URL := c.Param("OriginURL") + byteURL, err := base64.StdEncoding.DecodeString(b64URL) + if err != nil { + log.Println(err) + } + + ffmpeg := os.Getenv("FFMPEG_BIN") + if ffmpeg == "" { + ffmpeg = "ffmpeg" + } + + // get service id + var serviceID string + input := string(byteURL) + if test := os.Getenv("TEST_INPUT"); test != "" { + input = test + serviceID = "26624" + } else { + inputParsed, _ := url.Parse(input) + serviceID = strings.Split(inputParsed.Path, "/")[6] + log.Println(input) + } + + base := "http://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + "/api/programs" + if os.Getenv("MIRAKURUN_HTTPS") == "true" { + base = "https://" + os.Getenv("MIRAKURUN_IP") + ":" + os.Getenv("MIRAKURUN_PORT") + "/api/programs" + } + log.Println(base) + + // get program from mirakurun + // TODO: xmltv should contain dualmonostereo info + req, _ := http.NewRequest("GET", base, nil) + q := req.URL.Query() + q.Add("serviceId", serviceID) + req.URL.RawQuery = q.Encode() + client := new(http.Client) + res, err := client.Do(req) + if err != nil { + log.Println(err) + } + defer res.Body.Close() + var programs entities.MirakurunPrograms + jsonDecoder := json.NewDecoder(res.Body) + err = jsonDecoder.Decode(&programs) + + // find audio component type + var isdualmonostereo bool = false + cursor2 := time.Now().Add(5*time.Minute).UnixNano() / 1000000 + for _, v := range programs { + if cursor2 > v.StartAt && cursor2 < v.StartAt+int64(v.Duration) { + if v.Audio.ComponentType == 2 { + isdualmonostereo = true + } + } + } + + xmltv := app.ConvertEpgToXML() + + var subtitle string = os.Getenv("SUBTITLE_DIR") + if subtitle == "" { + log.Fatal("set SUBTITLE_DIR") + } + + // search on air program cursor + // TODO: handle program name collectly + var tmpsubtitle string + var lapisID string + cursor := time.Now().Add(5 * time.Minute).Format(app.Xmldateformat) + log.Println(cursor) + for _, v := range xmltv.Programme { + if v.Channel == serviceID { + if cursor > v.Start && cursor < v.Stop { + base := subtitle + + lapisID = strings.Split(v.Desc.Desc, "lapisID:")[1] + fname := lapisID + "-" + v.Title.Title + "-" + fmt.Sprint(v.Start) + "-" + time.Now().In(jst).Format(time.RFC3339Nano) + subtitle = path.Join(base, fname) + log.Println("found!!!", v.Title.Title, v.Channel, lapisID) + + // b := base64.StdEncoding.EncodeToString([]byte(v.Name)) + fname = time.Now().Format(time.RFC3339Nano) + tmpsubtitle = path.Join(base, fname) + } + } + } + + // cmd := exec.Command(ffmpeg, "-re", + // "-i", input, "-c:v copy -acodec ac3 -b:a 192k -f mpegts pipe:1", + // "-vf -re -c:s webvtt", subtitle) + + // ffmpeg -re -i input -c:v copy -c:a copy -f mpegts pipe:1 + // -vf -re -c:s webvtt subtitle + + // cmd := exec.Command(ffmpeg, "-re", + // "-fix_sub_duration", + // "-i", input, "-c:v", "libx264", + // "-s", "720x480", "-aspect", "16:9", "-vb", "3000k", + // "-acodec", "ac3", "-b:a", "192k", + // "-c:s", "webvtt", + // "-f", "webm", "pipe:1") + + // cmd := exec.Command(ffmpeg, "-re", + // "-fix_sub_duration", + // "-i", input, "-c:v", "copy", + // "-acodec", "ac3", "-b:a", "192k", + // "-c:s", "mov_text", + // "-movflags", "frag_keyframe+empty_moov+delay_moov", + // "-f", "mp4", + // "pipe:1") + + // cmd := exec.Command(ffmpeg, "-re", + // "-fix_sub_duration", + // "-i", input, "-c:v", "copy", + // "-c:a", "copy", + // "-c:s", "webvtt", + // "-f", "mpegts", "pipe:1") + + // cmd := exec.Command(ffmpeg, "-re", + // "-fix_sub_duration", + // "-i", input, "-c:v", "copy", + // "-acodec", "ac3", "-b:a", "192k", + // "-c:s", "webvtt", + // "-f", "matroska", "pipe:1") + + // https://unix.stackexchange.com/questions/28503/how-can-i-send-stdout-to-multiple-commands + reqOriginStream, _ := http.NewRequest("GET", input, nil) + resOriginStream, err := client.Do(reqOriginStream) + if err != nil { + log.Println("get Mirakurun stream err:", err) + } + defer resOriginStream.Body.Close() + + var ffmpegSubtitle *exec.Cmd + + // backgroud ffmpeg task + if strings.Contains(subtitle, "[字]") { + tmpsubtitle += ".mkv" + // clearly contains subtitle + // webvtt generated by ffmpeg does not support color(?) + ffmpegSubtitle = exec.Command(ffmpeg, + "-analyzeduration", "5MB", + "-probesize", "5MB", + "-fix_sub_duration", + "-i", "pipe:0", + "-c:v", "copy", + "-acodec", "ac3", "-b:a", "192k", + "-c:s", "webvtt", + "-f", "matroska", tmpsubtitle) + } else { + tmpsubtitle += ".ass" + // run default + ffmpegSubtitle = exec.Command(ffmpeg, + "-analyzeduration", "5MB", + "-probesize", "5MB", + "-fix_sub_duration", + "-i", "pipe:0", + "-c:s", "ass", "-f", "ass", tmpsubtitle) + } + + ffmpegSubtitleStdin, _ := ffmpegSubtitle.StdinPipe() + ffmpegSubtitle.Stdout = os.Stdout + ffmpegSubtitle.Stderr = os.Stderr + + ffmpegSubtitle.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + ffmpegSubtitle.Start() + + log.Println(ffmpegSubtitle.String()) + + // defer func must declare before the command start + defer func() { + log.Println("post process start") + // ffmpegSubtitle.Process.Kill() + gpid, err := syscall.Getpgid(ffmpegSubtitle.Process.Pid) + if err == nil { + syscall.Kill(-gpid, 15) + } + ffmpegSubtitle.Wait() + c.Request.Body.Close() + log.Println("post process end") + }() + + header := c.Writer.Header() + header["Content-type"] = []string{"video/M2PT"} + header["Server"] = []string{"lapis/" + entities.LapisVersion} + header["Content-Disposition"] = []string{`attachment; filename=lapis.m2ts`} + + sw, err := stream.New("m2tsstream") + if err != nil { + log.Fatal(err) + } + + // input stream + go func() { + log.Println("input m2ts stream") + io.Copy(sw, resOriginStream.Body) + log.Println("input m2ts stream destoroy") + // sw.Close() + }() + + waiterForSubtitleWriter := make(chan struct{}) + waiterForResponseBodyReader := make(chan struct{}) + + // stdin ffmpeg subtitle + go func() { + r, err := sw.NextReader() + if err != nil { + log.Fatal(err) + } + defer r.Close() + log.Println("ffmpeg stdin will start...") + + close(waiterForSubtitleWriter) + io.Copy(ffmpegSubtitleStdin, r) + ffmpegSubtitleStdin.Close() + log.Println("ffmpeg stdin end") + }() + // go io.Copy(fp, ffmpegSubtitleStdout) + + // write to response body + r, err := sw.NextReader() + if err != nil { + log.Fatal(err) + } + defer r.Close() + + log.Println("body write will start...") + // if dual mono stereo + if isdualmonostereo { + log.Println("DUAL MONO STEREO") + ffmpegDualMonoStereo := exec.Command(ffmpeg, + "-re", + "-analyzeduration", "2MB", + "-probesize", "2MB", + "-fix_sub_duration", + "-dual_mono_mode", "main", + "-i", "pipe:0", + "-c:v", "copy", + "-acodec", "ac3", "-b:a", "192k", + "-c:s", "webvtt", + "-f", "mpegts", "pipe:1") + + stdin, _ := ffmpegDualMonoStereo.StdinPipe() + stdout, _ := ffmpegDualMonoStereo.StdoutPipe() + ffmpegDualMonoStereo.Stdout = os.Stdout + ffmpegDualMonoStereo.Stderr = os.Stderr + ffmpegDualMonoStereo.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + ffmpegSubtitle.Start() + + defer func() { + log.Println("post process2 start") + // ffmpegSubtitle.Process.Kill() + gpid, err := syscall.Getpgid(ffmpegDualMonoStereo.Process.Pid) + if err == nil { + syscall.Kill(-gpid, 15) + } + ffmpegSubtitle.Wait() + c.Request.Body.Close() + log.Println("post process2 end") + }() + + go func() { + io.Copy(stdin, r) + stdin.Close() + }() + io.Copy(c.Writer, stdout) + } else { + io.Copy(c.Writer, r) + } + close(waiterForResponseBodyReader) + // <-waiterForResponseBodyReader + log.Println("subtitle wait start...") + + // SIGINT + gpid, err := syscall.Getpgid(ffmpegSubtitle.Process.Pid) + if err == nil { + syscall.Kill(-gpid, 2) + } + ffmpegSubtitle.Wait() + stdout, err := ffmpegSubtitle.CombinedOutput() + log.Println("progout", err, string(stdout)) + + sw.Close() + log.Println("end prog") + + if strings.Contains(subtitle, "[字]") { + tmpfile, err := ioutil.TempFile("", "mergetmp") + if err != nil { + log.Fatal(err) + } + + r, err := sw.NextReader() + if err != nil { + log.Fatal(err) + } + defer r.Close() + + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + io.Copy(tmpfile, r) + } + // if strings.Contains(subtitle, "[字]") { + // r, err := sw.NextReader() + // if err != nil { + // log.Println("Err at concatsubtitle", err) + // } + // ffmpegConcatSubtitle := exec.Command(ffmpeg, + // "-analyzeduration", "2MB", + // "-probesize", "2MB", + // "-fix_sub_duration", + // "-dual_mono_mode", "main", + // "-i", "pipe:0", + // "-i", tmpsubtitle, + // "-c:v", "copy", + // "-acodec", "ac3", "-b:a", "192k", + // "-c:s", "ass", + // "-f", "matroska", tmpsubtitle+".mkv") + + // stdin, _ := ffmpegConcatSubtitle.StdinPipe() + // ffmpegConcatSubtitle.Stdout = os.Stdout + // ffmpegConcatSubtitle.Stderr = os.Stderr + // ffmpegConcatSubtitle.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + // ffmpegSubtitle.Start() + + // defer func() { + // log.Println("post process2 start") + // // ffmpegSubtitle.Process.Kill() + // gpid, err := syscall.Getpgid(ffmpegConcatSubtitle.Process.Pid) + // if err == nil { + // syscall.Kill(-gpid, 15) + // } + // ffmpegSubtitle.Wait() + // c.Request.Body.Close() + // log.Println("post process2 end") + // }() + + // go func() { + // io.Copy(stdin, r) + // stdin.Close() + // }() + // os.Remove(tmpsubtitle) + // tmpsubtitle += ".mkv" + // } + + if _, err := os.Stat(tmpsubtitle); err == nil { + // if file contains "WEBVTT" only, remove + if readAllLines(tmpsubtitle) > 1 { + pos := strings.LastIndex(tmpsubtitle, ".") + err = os.Rename(tmpsubtitle, subtitle+tmpsubtitle[pos:]) + if err != nil { + log.Println(err) + } + } else { + os.Remove(tmpsubtitle) + } + } +} + +func readAllLines(filename string) int { + fp, err := os.Open(filename) + if err != nil { + return 0 + } + + lines := 0 + + scanner := bufio.NewScanner(fp) + for scanner.Scan() { + lines++ + } + + fp.Close() + return lines +}