From bd7cc9a893f88fd9dbf1fdc72e1d81f3e2afb96f Mon Sep 17 00:00:00 2001 From: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com> Date: Thu, 18 Mar 2021 19:56:40 +0100 Subject: [PATCH] Add support for m3u8 playlist (#76) Signed-off-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com> --- pkg/server/handlers.go | 80 +++++++------------------------------ pkg/server/routes.go | 24 ++++------- pkg/server/server.go | 15 +++++-- pkg/server/xtreamHandles.go | 78 +++++++++++++++++++++++++++++++++--- 4 files changed, 105 insertions(+), 92 deletions(-) diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index 670368e3..baf999a0 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -20,13 +20,13 @@ package server import ( "bytes" - "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" + "path" "strings" "time" @@ -50,13 +50,19 @@ func (c *Config) reverseProxy(ctx *gin.Context) { c.stream(ctx, rpURL) } -func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) { +func (c *Config) m3u8ReverseProxy(ctx *gin.Context) { id := ctx.Param("id") - if strings.HasSuffix(id, ".m3u8") { - c.hlsStream(ctx, oriURL) + + rpURL, err := url.Parse(strings.ReplaceAll(c.track.URI, path.Base(c.track.URI), id)) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck return } + c.stream(ctx, rpURL) +} + +func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) { client := &http.Client{} req, err := http.NewRequest("GET", oriURL.String(), nil) @@ -82,70 +88,14 @@ func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) { }) } -func (c *Config) hlsStream(ctx *gin.Context, oriURL *url.URL) { - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - req, err := http.NewRequest("GET", oriURL.String(), nil) - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - - req.Header.Set("User-Agent", ctx.Request.UserAgent()) - - resp, err := client.Do(req) - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusFound { - location, err := resp.Location() - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - id := ctx.Param("id") - if strings.Contains(location.String(), id) { - hlsChannelsRedirectURLLock.Lock() - hlsChannelsRedirectURL[id] = *location - hlsChannelsRedirectURLLock.Unlock() - - hlsReq, err := http.NewRequest("GET", location.String(), nil) - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - - hlsReq.Header.Set("User-Agent", ctx.Request.UserAgent()) - - hlsResp, err := client.Do(hlsReq) - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - defer hlsResp.Body.Close() - - b, err := ioutil.ReadAll(hlsResp.Body) - if err != nil { - ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck - return - } - body := string(b) - body = strings.ReplaceAll(body, "/"+c.XtreamUser.String()+"/"+c.XtreamPassword.String()+"/", "/"+c.User.String()+"/"+c.Password.String()+"/") - ctx.Data(http.StatusOK, hlsResp.Header.Get("Content-Type"), []byte(body)) - return - } - ctx.AbortWithError(http.StatusInternalServerError, errors.New("Unable to HLS stream")) // nolint: errcheck +func (c *Config) xtreamStream(ctx *gin.Context, oriURL *url.URL) { + id := ctx.Param("id") + if strings.HasSuffix(id, ".m3u8") { + c.hlsXtreamStream(ctx, oriURL) return } - ctx.Status(resp.StatusCode) + c.stream(ctx, oriURL) } func copyHTTPHeader(ctx *gin.Context, header http.Header) { diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 5bd226ca..30f2e5db 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -20,8 +20,7 @@ package server import ( "fmt" - "log" - "net/url" + "path" "strings" "github.com/gin-gonic/gin" @@ -54,7 +53,7 @@ func (c *Config) xtreamRoutes(r *gin.RouterGroup) { r.GET("/player_api.php", c.authenticate, c.xtreamPlayerAPIGET) r.POST("/player_api.php", c.appAuthenticate, c.xtreamPlayerAPIPOST) r.GET("/xmltv.php", c.authenticate, c.xtreamXMLTV) - r.GET(fmt.Sprintf("/%s/%s/:id", c.User, c.Password), c.xtreamStream) + r.GET(fmt.Sprintf("/%s/%s/:id", c.User, c.Password), c.xtreamStreamHandler) r.GET(fmt.Sprintf("/live/%s/%s/:id", c.User, c.Password), c.xtreamStreamLive) r.GET(fmt.Sprintf("/movie/%s/%s/:id", c.User, c.Password), c.xtreamStreamMovie) r.GET(fmt.Sprintf("/series/%s/%s/:id", c.User, c.Password), c.xtreamStreamSeries) @@ -66,25 +65,16 @@ func (c *Config) m3uRoutes(r *gin.RouterGroup) { // XXX Private need: for external Android app r.POST("/"+c.M3UFileName, c.authenticate, c.getM3U) - // List to verify duplicate entry endpoints - checkList := map[string]int8{} for i, track := range c.playlist.Tracks { - oriURL, err := url.Parse(track.URI) - if err != nil { - return - } trackConfig := &Config{ ProxyConfig: c.ProxyConfig, track: &c.playlist.Tracks[i], } - _, ok := checkList[oriURL.Path] - if ok { - log.Printf("[iptv-proxy] WARNING endpoint %q already exist, skipping it", oriURL.Path) - continue - } - r.GET(fmt.Sprintf("/%s/%s/%s", c.User, c.Password, oriURL.Path), trackConfig.reverseProxy) - - checkList[oriURL.Path] = 0 + if strings.HasSuffix(track.URI, ".m3u8") { + r.GET(fmt.Sprintf("/%s/%s/%d/:id", c.User, c.Password, i), trackConfig.m3u8ReverseProxy) + } else { + r.GET(fmt.Sprintf("/%s/%s/%d/%s", c.User, c.Password, i, path.Base(track.URI)), trackConfig.reverseProxy) + } } } diff --git a/pkg/server/server.go b/pkg/server/server.go index bfbbf7bb..678b1b73 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -98,8 +98,11 @@ func (c *Config) playlistInitialization() error { // MarshallInto a *bufio.Writer a Playlist. func (c *Config) marshallInto(into *os.File, xtream bool) error { + filteredTrack := make([]m3u.Track, 0, len(c.playlist.Tracks)) + + ret := 0 into.WriteString("#EXTM3U\n") // nolint: errcheck - for _, track := range c.playlist.Tracks { + for i, track := range c.playlist.Tracks { var buffer bytes.Buffer buffer.WriteString("#EXTINF:") // nolint: errcheck @@ -112,20 +115,24 @@ func (c *Config) marshallInto(into *os.File, xtream bool) error { buffer.WriteString(fmt.Sprintf("%s=%q ", track.Tags[i].Name, track.Tags[i].Value)) // nolint: errcheck } - uri, err := c.replaceURL(track.URI, xtream) + uri, err := c.replaceURL(track.URI, i-ret, xtream) if err != nil { + ret++ log.Printf("ERROR: track: %s: %s", track.Name, err) continue } into.WriteString(fmt.Sprintf("%s, %s\n%s\n", buffer.String(), track.Name, uri)) // nolint: errcheck + + filteredTrack = append(filteredTrack, track) } + c.playlist.Tracks = filteredTrack return into.Sync() } // ReplaceURL replace original playlist url by proxy url -func (c *Config) replaceURL(uri string, xtream bool) (string, error) { +func (c *Config) replaceURL(uri string, trackIndex int, xtream bool) (string, error) { oriURL, err := url.Parse(uri) if err != nil { return "", err @@ -146,7 +153,7 @@ func (c *Config) replaceURL(uri string, xtream bool) (string, error) { uriPath = strings.ReplaceAll(uriPath, c.XtreamUser.PathEscape(), c.User.PathEscape()) uriPath = strings.ReplaceAll(uriPath, c.XtreamPassword.PathEscape(), c.Password.PathEscape()) } else { - uriPath = path.Join("/", c.User.PathEscape(), c.Password.PathEscape(), uriPath) + uriPath = path.Join("/", c.User.PathEscape(), c.Password.PathEscape(), fmt.Sprintf("%d", trackIndex), path.Base(uriPath)) } basicAuth := oriURL.User.String() diff --git a/pkg/server/xtreamHandles.go b/pkg/server/xtreamHandles.go index 6665aa50..6c179a05 100644 --- a/pkg/server/xtreamHandles.go +++ b/pkg/server/xtreamHandles.go @@ -198,7 +198,7 @@ func (c *Config) xtreamXMLTV(ctx *gin.Context) { ctx.Data(http.StatusOK, "application/xml", resp) } -func (c *Config) xtreamStream(ctx *gin.Context) { +func (c *Config) xtreamStreamHandler(ctx *gin.Context) { id := ctx.Param("id") rpURL, err := url.Parse(fmt.Sprintf("%s/%s/%s/%s", c.XtreamBaseURL, c.XtreamUser, c.XtreamPassword, id)) if err != nil { @@ -206,7 +206,7 @@ func (c *Config) xtreamStream(ctx *gin.Context) { return } - c.stream(ctx, rpURL) + c.xtreamStream(ctx, rpURL) } func (c *Config) xtreamStreamLive(ctx *gin.Context) { @@ -217,7 +217,7 @@ func (c *Config) xtreamStreamLive(ctx *gin.Context) { return } - c.stream(ctx, rpURL) + c.xtreamStream(ctx, rpURL) } func (c *Config) xtreamStreamMovie(ctx *gin.Context) { @@ -228,7 +228,7 @@ func (c *Config) xtreamStreamMovie(ctx *gin.Context) { return } - c.stream(ctx, rpURL) + c.xtreamStream(ctx, rpURL) } func (c *Config) xtreamStreamSeries(ctx *gin.Context) { @@ -239,7 +239,7 @@ func (c *Config) xtreamStreamSeries(ctx *gin.Context) { return } - c.stream(ctx, rpURL) + c.xtreamStream(ctx, rpURL) } func (c *Config) hlsrStream(ctx *gin.Context) { @@ -271,5 +271,71 @@ func (c *Config) hlsrStream(ctx *gin.Context) { return } - c.stream(ctx, req) + c.xtreamStream(ctx, req) +} + +func (c *Config) hlsXtreamStream(ctx *gin.Context, oriURL *url.URL) { + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + req, err := http.NewRequest("GET", oriURL.String(), nil) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + + req.Header.Set("User-Agent", ctx.Request.UserAgent()) + + resp, err := client.Do(req) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusFound { + location, err := resp.Location() + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + id := ctx.Param("id") + if strings.Contains(location.String(), id) { + hlsChannelsRedirectURLLock.Lock() + hlsChannelsRedirectURL[id] = *location + hlsChannelsRedirectURLLock.Unlock() + + hlsReq, err := http.NewRequest("GET", location.String(), nil) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + + hlsReq.Header.Set("User-Agent", ctx.Request.UserAgent()) + + hlsResp, err := client.Do(hlsReq) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + defer hlsResp.Body.Close() + + b, err := ioutil.ReadAll(hlsResp.Body) + if err != nil { + ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck + return + } + body := string(b) + body = strings.ReplaceAll(body, "/"+c.XtreamUser.String()+"/"+c.XtreamPassword.String()+"/", "/"+c.User.String()+"/"+c.Password.String()+"/") + ctx.Data(http.StatusOK, hlsResp.Header.Get("Content-Type"), []byte(body)) + return + } + ctx.AbortWithError(http.StatusInternalServerError, errors.New("Unable to HLS stream")) // nolint: errcheck + return + } + + ctx.Status(resp.StatusCode) }