Skip to content

Commit

Permalink
Add support for m3u8 playlist (#76)
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre-Emmanuel Jacquier <[email protected]>
  • Loading branch information
pierre-emmanuelJ authored Mar 18, 2021
1 parent edb56e3 commit bd7cc9a
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 92 deletions.
80 changes: 15 additions & 65 deletions pkg/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ package server

import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"path"
"strings"
"time"

Expand All @@ -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)
Expand All @@ -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) {
Expand Down
24 changes: 7 additions & 17 deletions pkg/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ package server

import (
"fmt"
"log"
"net/url"
"path"
"strings"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
}
15 changes: 11 additions & 4 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down
78 changes: 72 additions & 6 deletions pkg/server/xtreamHandles.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,15 @@ 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 {
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
return
}

c.stream(ctx, rpURL)
c.xtreamStream(ctx, rpURL)
}

func (c *Config) xtreamStreamLive(ctx *gin.Context) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}

0 comments on commit bd7cc9a

Please sign in to comment.