diff --git a/cmd/root.go b/cmd/root.go index e38006bb..189ec9f2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 { @@ -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") diff --git a/pkg/config/config.go b/pkg/config/config.go index fd3d5390..9024a587 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 8ce6b4b4..5b321ec7 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -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) diff --git a/pkg/server/xtreamHandles.go b/pkg/server/xtreamHandles.go index b4553302..b24c9730 100644 --- a/pkg/server/xtreamHandles.go +++ b/pkg/server/xtreamHandles.go @@ -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) @@ -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() @@ -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 } @@ -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()) }