Skip to content

Commit bd7cc9a

Browse files
Add support for m3u8 playlist (#76)
Signed-off-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com>
1 parent edb56e3 commit bd7cc9a

File tree

4 files changed

+105
-92
lines changed

4 files changed

+105
-92
lines changed

pkg/server/handlers.go

Lines changed: 15 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ package server
2020

2121
import (
2222
"bytes"
23-
"errors"
2423
"fmt"
2524
"io"
2625
"io/ioutil"
2726
"log"
2827
"net/http"
2928
"net/url"
29+
"path"
3030
"strings"
3131
"time"
3232

@@ -50,13 +50,19 @@ func (c *Config) reverseProxy(ctx *gin.Context) {
5050
c.stream(ctx, rpURL)
5151
}
5252

53-
func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) {
53+
func (c *Config) m3u8ReverseProxy(ctx *gin.Context) {
5454
id := ctx.Param("id")
55-
if strings.HasSuffix(id, ".m3u8") {
56-
c.hlsStream(ctx, oriURL)
55+
56+
rpURL, err := url.Parse(strings.ReplaceAll(c.track.URI, path.Base(c.track.URI), id))
57+
if err != nil {
58+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
5759
return
5860
}
5961

62+
c.stream(ctx, rpURL)
63+
}
64+
65+
func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) {
6066
client := &http.Client{}
6167

6268
req, err := http.NewRequest("GET", oriURL.String(), nil)
@@ -82,70 +88,14 @@ func (c *Config) stream(ctx *gin.Context, oriURL *url.URL) {
8288
})
8389
}
8490

85-
func (c *Config) hlsStream(ctx *gin.Context, oriURL *url.URL) {
86-
client := &http.Client{
87-
CheckRedirect: func(req *http.Request, via []*http.Request) error {
88-
return http.ErrUseLastResponse
89-
},
90-
}
91-
92-
req, err := http.NewRequest("GET", oriURL.String(), nil)
93-
if err != nil {
94-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
95-
return
96-
}
97-
98-
req.Header.Set("User-Agent", ctx.Request.UserAgent())
99-
100-
resp, err := client.Do(req)
101-
if err != nil {
102-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
103-
return
104-
}
105-
defer resp.Body.Close()
106-
107-
if resp.StatusCode == http.StatusFound {
108-
location, err := resp.Location()
109-
if err != nil {
110-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
111-
return
112-
}
113-
id := ctx.Param("id")
114-
if strings.Contains(location.String(), id) {
115-
hlsChannelsRedirectURLLock.Lock()
116-
hlsChannelsRedirectURL[id] = *location
117-
hlsChannelsRedirectURLLock.Unlock()
118-
119-
hlsReq, err := http.NewRequest("GET", location.String(), nil)
120-
if err != nil {
121-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
122-
return
123-
}
124-
125-
hlsReq.Header.Set("User-Agent", ctx.Request.UserAgent())
126-
127-
hlsResp, err := client.Do(hlsReq)
128-
if err != nil {
129-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
130-
return
131-
}
132-
defer hlsResp.Body.Close()
133-
134-
b, err := ioutil.ReadAll(hlsResp.Body)
135-
if err != nil {
136-
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
137-
return
138-
}
139-
body := string(b)
140-
body = strings.ReplaceAll(body, "/"+c.XtreamUser.String()+"/"+c.XtreamPassword.String()+"/", "/"+c.User.String()+"/"+c.Password.String()+"/")
141-
ctx.Data(http.StatusOK, hlsResp.Header.Get("Content-Type"), []byte(body))
142-
return
143-
}
144-
ctx.AbortWithError(http.StatusInternalServerError, errors.New("Unable to HLS stream")) // nolint: errcheck
91+
func (c *Config) xtreamStream(ctx *gin.Context, oriURL *url.URL) {
92+
id := ctx.Param("id")
93+
if strings.HasSuffix(id, ".m3u8") {
94+
c.hlsXtreamStream(ctx, oriURL)
14595
return
14696
}
14797

148-
ctx.Status(resp.StatusCode)
98+
c.stream(ctx, oriURL)
14999
}
150100

151101
func copyHTTPHeader(ctx *gin.Context, header http.Header) {

pkg/server/routes.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ package server
2020

2121
import (
2222
"fmt"
23-
"log"
24-
"net/url"
23+
"path"
2524
"strings"
2625

2726
"github.com/gin-gonic/gin"
@@ -54,7 +53,7 @@ func (c *Config) xtreamRoutes(r *gin.RouterGroup) {
5453
r.GET("/player_api.php", c.authenticate, c.xtreamPlayerAPIGET)
5554
r.POST("/player_api.php", c.appAuthenticate, c.xtreamPlayerAPIPOST)
5655
r.GET("/xmltv.php", c.authenticate, c.xtreamXMLTV)
57-
r.GET(fmt.Sprintf("/%s/%s/:id", c.User, c.Password), c.xtreamStream)
56+
r.GET(fmt.Sprintf("/%s/%s/:id", c.User, c.Password), c.xtreamStreamHandler)
5857
r.GET(fmt.Sprintf("/live/%s/%s/:id", c.User, c.Password), c.xtreamStreamLive)
5958
r.GET(fmt.Sprintf("/movie/%s/%s/:id", c.User, c.Password), c.xtreamStreamMovie)
6059
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) {
6665
// XXX Private need: for external Android app
6766
r.POST("/"+c.M3UFileName, c.authenticate, c.getM3U)
6867

69-
// List to verify duplicate entry endpoints
70-
checkList := map[string]int8{}
7168
for i, track := range c.playlist.Tracks {
72-
oriURL, err := url.Parse(track.URI)
73-
if err != nil {
74-
return
75-
}
7669
trackConfig := &Config{
7770
ProxyConfig: c.ProxyConfig,
7871
track: &c.playlist.Tracks[i],
7972
}
80-
_, ok := checkList[oriURL.Path]
81-
if ok {
82-
log.Printf("[iptv-proxy] WARNING endpoint %q already exist, skipping it", oriURL.Path)
83-
continue
84-
}
8573

86-
r.GET(fmt.Sprintf("/%s/%s/%s", c.User, c.Password, oriURL.Path), trackConfig.reverseProxy)
87-
88-
checkList[oriURL.Path] = 0
74+
if strings.HasSuffix(track.URI, ".m3u8") {
75+
r.GET(fmt.Sprintf("/%s/%s/%d/:id", c.User, c.Password, i), trackConfig.m3u8ReverseProxy)
76+
} else {
77+
r.GET(fmt.Sprintf("/%s/%s/%d/%s", c.User, c.Password, i, path.Base(track.URI)), trackConfig.reverseProxy)
78+
}
8979
}
9080
}

pkg/server/server.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ func (c *Config) playlistInitialization() error {
9898

9999
// MarshallInto a *bufio.Writer a Playlist.
100100
func (c *Config) marshallInto(into *os.File, xtream bool) error {
101+
filteredTrack := make([]m3u.Track, 0, len(c.playlist.Tracks))
102+
103+
ret := 0
101104
into.WriteString("#EXTM3U\n") // nolint: errcheck
102-
for _, track := range c.playlist.Tracks {
105+
for i, track := range c.playlist.Tracks {
103106
var buffer bytes.Buffer
104107

105108
buffer.WriteString("#EXTINF:") // nolint: errcheck
@@ -112,20 +115,24 @@ func (c *Config) marshallInto(into *os.File, xtream bool) error {
112115
buffer.WriteString(fmt.Sprintf("%s=%q ", track.Tags[i].Name, track.Tags[i].Value)) // nolint: errcheck
113116
}
114117

115-
uri, err := c.replaceURL(track.URI, xtream)
118+
uri, err := c.replaceURL(track.URI, i-ret, xtream)
116119
if err != nil {
120+
ret++
117121
log.Printf("ERROR: track: %s: %s", track.Name, err)
118122
continue
119123
}
120124

121125
into.WriteString(fmt.Sprintf("%s, %s\n%s\n", buffer.String(), track.Name, uri)) // nolint: errcheck
126+
127+
filteredTrack = append(filteredTrack, track)
122128
}
129+
c.playlist.Tracks = filteredTrack
123130

124131
return into.Sync()
125132
}
126133

127134
// ReplaceURL replace original playlist url by proxy url
128-
func (c *Config) replaceURL(uri string, xtream bool) (string, error) {
135+
func (c *Config) replaceURL(uri string, trackIndex int, xtream bool) (string, error) {
129136
oriURL, err := url.Parse(uri)
130137
if err != nil {
131138
return "", err
@@ -146,7 +153,7 @@ func (c *Config) replaceURL(uri string, xtream bool) (string, error) {
146153
uriPath = strings.ReplaceAll(uriPath, c.XtreamUser.PathEscape(), c.User.PathEscape())
147154
uriPath = strings.ReplaceAll(uriPath, c.XtreamPassword.PathEscape(), c.Password.PathEscape())
148155
} else {
149-
uriPath = path.Join("/", c.User.PathEscape(), c.Password.PathEscape(), uriPath)
156+
uriPath = path.Join("/", c.User.PathEscape(), c.Password.PathEscape(), fmt.Sprintf("%d", trackIndex), path.Base(uriPath))
150157
}
151158

152159
basicAuth := oriURL.User.String()

pkg/server/xtreamHandles.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ func (c *Config) xtreamXMLTV(ctx *gin.Context) {
198198
ctx.Data(http.StatusOK, "application/xml", resp)
199199
}
200200

201-
func (c *Config) xtreamStream(ctx *gin.Context) {
201+
func (c *Config) xtreamStreamHandler(ctx *gin.Context) {
202202
id := ctx.Param("id")
203203
rpURL, err := url.Parse(fmt.Sprintf("%s/%s/%s/%s", c.XtreamBaseURL, c.XtreamUser, c.XtreamPassword, id))
204204
if err != nil {
205205
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
206206
return
207207
}
208208

209-
c.stream(ctx, rpURL)
209+
c.xtreamStream(ctx, rpURL)
210210
}
211211

212212
func (c *Config) xtreamStreamLive(ctx *gin.Context) {
@@ -217,7 +217,7 @@ func (c *Config) xtreamStreamLive(ctx *gin.Context) {
217217
return
218218
}
219219

220-
c.stream(ctx, rpURL)
220+
c.xtreamStream(ctx, rpURL)
221221
}
222222

223223
func (c *Config) xtreamStreamMovie(ctx *gin.Context) {
@@ -228,7 +228,7 @@ func (c *Config) xtreamStreamMovie(ctx *gin.Context) {
228228
return
229229
}
230230

231-
c.stream(ctx, rpURL)
231+
c.xtreamStream(ctx, rpURL)
232232
}
233233

234234
func (c *Config) xtreamStreamSeries(ctx *gin.Context) {
@@ -239,7 +239,7 @@ func (c *Config) xtreamStreamSeries(ctx *gin.Context) {
239239
return
240240
}
241241

242-
c.stream(ctx, rpURL)
242+
c.xtreamStream(ctx, rpURL)
243243
}
244244

245245
func (c *Config) hlsrStream(ctx *gin.Context) {
@@ -271,5 +271,71 @@ func (c *Config) hlsrStream(ctx *gin.Context) {
271271
return
272272
}
273273

274-
c.stream(ctx, req)
274+
c.xtreamStream(ctx, req)
275+
}
276+
277+
func (c *Config) hlsXtreamStream(ctx *gin.Context, oriURL *url.URL) {
278+
client := &http.Client{
279+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
280+
return http.ErrUseLastResponse
281+
},
282+
}
283+
284+
req, err := http.NewRequest("GET", oriURL.String(), nil)
285+
if err != nil {
286+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
287+
return
288+
}
289+
290+
req.Header.Set("User-Agent", ctx.Request.UserAgent())
291+
292+
resp, err := client.Do(req)
293+
if err != nil {
294+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
295+
return
296+
}
297+
defer resp.Body.Close()
298+
299+
if resp.StatusCode == http.StatusFound {
300+
location, err := resp.Location()
301+
if err != nil {
302+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
303+
return
304+
}
305+
id := ctx.Param("id")
306+
if strings.Contains(location.String(), id) {
307+
hlsChannelsRedirectURLLock.Lock()
308+
hlsChannelsRedirectURL[id] = *location
309+
hlsChannelsRedirectURLLock.Unlock()
310+
311+
hlsReq, err := http.NewRequest("GET", location.String(), nil)
312+
if err != nil {
313+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
314+
return
315+
}
316+
317+
hlsReq.Header.Set("User-Agent", ctx.Request.UserAgent())
318+
319+
hlsResp, err := client.Do(hlsReq)
320+
if err != nil {
321+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
322+
return
323+
}
324+
defer hlsResp.Body.Close()
325+
326+
b, err := ioutil.ReadAll(hlsResp.Body)
327+
if err != nil {
328+
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
329+
return
330+
}
331+
body := string(b)
332+
body = strings.ReplaceAll(body, "/"+c.XtreamUser.String()+"/"+c.XtreamPassword.String()+"/", "/"+c.User.String()+"/"+c.Password.String()+"/")
333+
ctx.Data(http.StatusOK, hlsResp.Header.Get("Content-Type"), []byte(body))
334+
return
335+
}
336+
ctx.AbortWithError(http.StatusInternalServerError, errors.New("Unable to HLS stream")) // nolint: errcheck
337+
return
338+
}
339+
340+
ctx.Status(resp.StatusCode)
275341
}

0 commit comments

Comments
 (0)