Skip to content

Commit

Permalink
Xtream: Genarate playlist from API live categories (#104)
Browse files Browse the repository at this point in the history
Signed-off-by: Pierre-Emmanuel Jacquier <15922119+pierre-emmanuelJ@users.noreply.github.com>
  • Loading branch information
pierre-emmanuelJ committed Jan 3, 2022
1 parent 4034a96 commit d48a875
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 33 deletions.
24 changes: 13 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,18 @@ var rootCmd = &cobra.Command{
Hostname: viper.GetString("hostname"),
Port: viper.GetInt("port"),
},
RemoteURL: remoteHostURL,
XtreamUser: config.CredentialString(xtreamUser),
XtreamPassword: config.CredentialString(xtreamPassword),
XtreamBaseURL: xtreamBaseURL,
M3UCacheExpiration: viper.GetInt("m3u-cache-expiration"),
User: config.CredentialString(viper.GetString("user")),
Password: config.CredentialString(viper.GetString("password")),
AdvertisedPort: viper.GetInt("advertised-port"),
HTTPS: viper.GetBool("https"),
M3UFileName: viper.GetString("m3u-file-name"),
CustomEndpoint: viper.GetString("custom-endpoint"),
RemoteURL: remoteHostURL,
XtreamUser: config.CredentialString(xtreamUser),
XtreamPassword: config.CredentialString(xtreamPassword),
XtreamBaseURL: xtreamBaseURL,
M3UCacheExpiration: viper.GetInt("m3u-cache-expiration"),
User: config.CredentialString(viper.GetString("user")),
Password: config.CredentialString(viper.GetString("password")),
AdvertisedPort: viper.GetInt("advertised-port"),
HTTPS: viper.GetBool("https"),
M3UFileName: viper.GetString("m3u-file-name"),
CustomEndpoint: viper.GetString("custom-endpoint"),
XtreamGenerateApiGet: viper.GetBool("xtream-api-get"),
}

if conf.AdvertisedPort == 0 {
Expand Down Expand Up @@ -130,6 +131,7 @@ func init() {
rootCmd.Flags().String("xtream-password", "", "Xtream-code password login")
rootCmd.Flags().String("xtream-base-url", "", "Xtream-code base url e.g(http://expample.tv:8080)")
rootCmd.Flags().Int("m3u-cache-expiration", 1, "M3U cache expiration in hour")
rootCmd.Flags().BoolP("xtream-api-get", "", false, "Generate get.php from xtream API instead of get.php original endpoint")

if e := viper.BindPFlags(rootCmd.Flags()); e != nil {
log.Fatal("error binding PFlags to viper")
Expand Down
23 changes: 12 additions & 11 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@ type HostConfiguration struct {

// ProxyConfig Contain original m3u playlist and HostConfiguration
type ProxyConfig struct {
HostConfig *HostConfiguration
XtreamUser CredentialString
XtreamPassword CredentialString
XtreamBaseURL string
M3UCacheExpiration int
M3UFileName string
CustomEndpoint string
RemoteURL *url.URL
AdvertisedPort int
HTTPS bool
User, Password CredentialString
HostConfig *HostConfiguration
XtreamUser CredentialString
XtreamPassword CredentialString
XtreamBaseURL string
XtreamGenerateApiGet bool
M3UCacheExpiration int
M3UFileName string
CustomEndpoint string
RemoteURL *url.URL
AdvertisedPort int
HTTPS bool
User, Password CredentialString
}
9 changes: 7 additions & 2 deletions pkg/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ func (c *Config) routes(r *gin.RouterGroup) {
}

func (c *Config) xtreamRoutes(r *gin.RouterGroup) {
r.GET("/get.php", c.authenticate, c.xtreamGet)
r.POST("/get.php", c.authenticate, c.xtreamGet)
getphp := gin.HandlerFunc(c.xtreamGet)
if c.XtreamGenerateApiGet {
getphp = c.xtreamApiGet
}
r.GET("/get.php", c.authenticate, getphp)
r.POST("/get.php", c.authenticate, getphp)
r.GET("/apiget", c.authenticate, c.xtreamApiGet)
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)
Expand Down
109 changes: 100 additions & 9 deletions pkg/server/xtreamHandles.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,12 @@ var hlsChannelsRedirectURLLock = sync.RWMutex{}
var xtreamM3uCache map[string]cacheMeta = map[string]cacheMeta{}
var xtreamM3uCacheLock = sync.RWMutex{}

func (c *Config) cacheXtreamM3u(m3uURL *url.URL) error {
func (c *Config) cacheXtreamM3u(playlist *m3u.Playlist, cacheName string) error {
xtreamM3uCacheLock.Lock()
defer xtreamM3uCacheLock.Unlock()

playlist, err := m3u.Parse(m3uURL.String())
if err != nil {
return err
}

tmp := *c
tmp.playlist = &playlist
tmp.playlist = playlist

path := filepath.Join(os.TempDir(), uuid.NewV4().String()+".iptv-proxy.m3u")
f, err := os.Create(path)
Expand All @@ -72,11 +67,63 @@ func (c *Config) cacheXtreamM3u(m3uURL *url.URL) error {
if err := tmp.marshallInto(f, true); err != nil {
return err
}
xtreamM3uCache[m3uURL.String()] = cacheMeta{path, time.Now()}
xtreamM3uCache[cacheName] = cacheMeta{path, time.Now()}

return nil
}

func (c *Config) xtreamGenerateM3u(ctx *gin.Context, extension string) (*m3u.Playlist, error) {
client, err := xtreamapi.New(c.XtreamUser.String(), c.XtreamPassword.String(), c.XtreamBaseURL, ctx.Request.UserAgent())
if err != nil {
return nil, err
}

cat, err := client.GetLiveCategories()
if err != nil {
return nil, err
}

// this is specific to xtream API,
// prefix with "live" if there is an extension.
var prefix string
if extension != "" {
prefix = "live/"
}

var playlist = new(m3u.Playlist)
playlist.Tracks = make([]m3u.Track, 0)

for _, category := range cat {
live, err := client.GetLiveStreams(fmt.Sprint(category.ID))
if err != nil {
return nil, err
}

for _, stream := range live {
track := m3u.Track{Name: stream.Name, Length: -1, URI: "", Tags: nil}

//TODO: Add more tag if needed.
if stream.EPGChannelID != "" {
track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-id", Value: stream.EPGChannelID})
}
if stream.Name != "" {
track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-name", Value: stream.Name})
}
if stream.Icon != "" {
track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-logo", Value: stream.Icon})
}
if category.Name != "" {
track.Tags = append(track.Tags, m3u.Tag{Name: "group-title", Value: category.Name})
}

track.URI = fmt.Sprintf("%s/%s%s/%s/%s.%s", c.XtreamBaseURL, prefix, c.XtreamUser, c.XtreamPassword, fmt.Sprint(stream.ID), extension)
playlist.Tracks = append(playlist.Tracks, track)
}
}

return playlist, nil
}

func (c *Config) xtreamGetAuto(ctx *gin.Context) {
newQuery := ctx.Request.URL.Query()
q := c.RemoteURL.Query()
Expand Down Expand Up @@ -117,7 +164,12 @@ func (c *Config) xtreamGet(ctx *gin.Context) {
if !ok || d.Hours() >= float64(c.M3UCacheExpiration) {
log.Printf("[iptv-proxy] %v | %s | xtream cache m3u file\n", time.Now().Format("2006/01/02 - 15:04:05"), ctx.ClientIP())
xtreamM3uCacheLock.RUnlock()
if err := c.cacheXtreamM3u(m3uURL); err != nil {
playlist, err := m3u.Parse(m3uURL.String())
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
return
}
if err := c.cacheXtreamM3u(&playlist, m3uURL.String()); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
return
}
Expand All @@ -134,6 +186,45 @@ func (c *Config) xtreamGet(ctx *gin.Context) {
ctx.File(path)
}

func (c *Config) xtreamApiGet(ctx *gin.Context) {
const (
apiGet = "apiget"
)

var (
extension = ctx.Query("output")
cacheName = apiGet + extension
)

xtreamM3uCacheLock.RLock()
meta, ok := xtreamM3uCache[cacheName]
d := time.Since(meta.Time)
if !ok || d.Hours() >= float64(c.M3UCacheExpiration) {
log.Printf("[iptv-proxy] %v | %s | xtream cache API m3u file\n", time.Now().Format("2006/01/02 - 15:04:05"), ctx.ClientIP())
xtreamM3uCacheLock.RUnlock()
playlist, err := c.xtreamGenerateM3u(ctx, extension)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
return
}
if err := c.cacheXtreamM3u(playlist, cacheName); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err) // nolint: errcheck
return
}
} else {
xtreamM3uCacheLock.RUnlock()
}

ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, c.M3UFileName))
xtreamM3uCacheLock.RLock()
path := xtreamM3uCache[cacheName].string
xtreamM3uCacheLock.RUnlock()
ctx.Header("Content-Type", "application/octet-stream")

ctx.File(path)

}

func (c *Config) xtreamPlayerAPIGET(ctx *gin.Context) {
c.xtreamPlayerAPI(ctx, ctx.Request.URL.Query())
}
Expand Down

0 comments on commit d48a875

Please sign in to comment.