Add support for m3u8 playlist (#76)

Signed-off-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com>
This commit is contained in:
Pierre-Emmanuel Jacquier
2021-03-18 19:56:40 +01:00
committed by GitHub
parent edb56e3abe
commit bd7cc9a893
4 changed files with 105 additions and 92 deletions

View File

@@ -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
func (c *Config) xtreamStream(ctx *gin.Context, oriURL *url.URL) {
id := ctx.Param("id")
if strings.HasSuffix(id, ".m3u8") {
c.hlsXtreamStream(ctx, oriURL)
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)
c.stream(ctx, oriURL)
}
func copyHTTPHeader(ctx *gin.Context, header http.Header) {

View File

@@ -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
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)
}
r.GET(fmt.Sprintf("/%s/%s/%s", c.User, c.Password, oriURL.Path), trackConfig.reverseProxy)
checkList[oriURL.Path] = 0
}
}

View File

@@ -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()

View File

@@ -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)
}