mirror of
https://github.com/pierre-emmanuelJ/iptv-proxy.git
synced 2026-03-12 14:13:59 +01:00
Add support for m3u8 playlist (#76)
Signed-off-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
edb56e3abe
commit
bd7cc9a893
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user