diff --git a/pkg/server/routes.go b/pkg/server/routes.go index ad54e68e..37fcc93e 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -48,10 +48,8 @@ func (c *Config) routes(r *gin.RouterGroup) { } func (c *Config) xtreamRoutes(r *gin.RouterGroup) { - getphp := gin.HandlerFunc(c.xtreamGet) - if c.XtreamGenerateApiGet { - getphp = c.xtreamApiGet - } + // Always use xtreamApiGet to include series and VOD + getphp := gin.HandlerFunc(c.xtreamApiGet) r.GET("/get.php", c.authenticate, getphp) r.POST("/get.php", c.authenticate, getphp) r.GET("/apiget", c.authenticate, c.xtreamApiGet) diff --git a/pkg/server/xtreamHandles.go b/pkg/server/xtreamHandles.go index f8ac670f..0ad63a53 100644 --- a/pkg/server/xtreamHandles.go +++ b/pkg/server/xtreamHandles.go @@ -73,33 +73,39 @@ func (c *Config) cacheXtreamM3u(playlist *m3u.Playlist, cacheName string) error } func (c *Config) xtreamGenerateM3u(ctx *gin.Context, extension string) (*m3u.Playlist, error) { + log.Printf("[iptv-proxy] xtreamGenerateM3u called with extension: %s", extension) + 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() + var playlist = new(m3u.Playlist) + playlist.Tracks = make([]m3u.Track, 0) + + // Add Live Streams + log.Printf("[iptv-proxy] Getting live categories...") + liveCat, err := client.GetLiveCategories() if err != nil { + log.Printf("[iptv-proxy] Error getting live categories: %v", err) return nil, err } + log.Printf("[iptv-proxy] Found %d live categories", len(liveCat)) // this is specific to xtream API, // prefix with "live" if there is an extension. - var prefix string + var livePrefix string if extension != "" { - extension = "." + extension - prefix = "live/" + livePrefix = "live/" } - var playlist = new(m3u.Playlist) - playlist.Tracks = make([]m3u.Track, 0) - - for _, category := range cat { + for _, category := range liveCat { live, err := client.GetLiveStreams(fmt.Sprint(category.ID)) if err != nil { return nil, err } + liveCount := 0 for _, stream := range live { track := m3u.Track{Name: stream.Name, Length: -1, URI: "", Tags: nil} @@ -117,11 +123,97 @@ func (c *Config) xtreamGenerateM3u(ctx *gin.Context, extension string) (*m3u.Pla 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) + var ext string + if extension != "" { + ext = "." + extension + } + track.URI = fmt.Sprintf("%s/%s%s/%s/%s%s", c.XtreamBaseURL, livePrefix, c.XtreamUser, c.XtreamPassword, fmt.Sprint(stream.ID), ext) + playlist.Tracks = append(playlist.Tracks, track) + liveCount++ + } + log.Printf("[iptv-proxy] Added %d live streams from category: %s", liveCount, category.Name) + } + + // Add VOD (Movies) + log.Printf("[iptv-proxy] Getting VOD categories...") + vodCat, err := client.GetVideoOnDemandCategories() + if err != nil { + log.Printf("[iptv-proxy] Error getting VOD categories: %v", err) + return nil, err + } + log.Printf("[iptv-proxy] Found %d VOD categories", len(vodCat)) + + for _, category := range vodCat { + vods, err := client.GetVideoOnDemandStreams(fmt.Sprint(category.ID)) + if err != nil { + return nil, err + } + + for _, vod := range vods { + track := m3u.Track{Name: vod.Name, Length: -1, URI: "", Tags: nil} + + //TODO: Add more tag if needed. + if vod.Name != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-name", Value: vod.Name}) + } + if vod.Icon != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-logo", Value: vod.Icon}) + } + if category.Name != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "group-title", Value: category.Name}) + } + + var ext string + if extension != "" { + ext = "." + extension + } + track.URI = fmt.Sprintf("%s/movie/%s/%s/%s%s", c.XtreamBaseURL, c.XtreamUser, c.XtreamPassword, fmt.Sprint(vod.ID), ext) + playlist.Tracks = append(playlist.Tracks, track) + } + } + + // Add Series + log.Printf("[iptv-proxy] Getting series categories...") + seriesCat, err := client.GetSeriesCategories() + if err != nil { + log.Printf("[iptv-proxy] Error getting series categories: %v", err) + return nil, err + } + log.Printf("[iptv-proxy] Found %d series categories", len(seriesCat)) + + for _, category := range seriesCat { + series, err := client.GetSeries(fmt.Sprint(category.ID)) + if err != nil { + return nil, err + } + + seriesCount := 0 + for _, serie := range series { + track := m3u.Track{Name: serie.Name, Length: -1, URI: "", Tags: nil} + + //TODO: Add more tag if needed. + if serie.Name != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-name", Value: serie.Name}) + } + if serie.Cover != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "tvg-logo", Value: serie.Cover}) + } + if category.Name != "" { + track.Tags = append(track.Tags, m3u.Tag{Name: "group-title", Value: category.Name}) + } + + var ext string + if extension != "" { + ext = "." + extension + } + track.URI = fmt.Sprintf("%s/series/%s/%s/%s%s", c.XtreamBaseURL, c.XtreamUser, c.XtreamPassword, fmt.Sprint(serie.SeriesID), ext) playlist.Tracks = append(playlist.Tracks, track) + seriesCount++ } + log.Printf("[iptv-proxy] Added %d series from category: %s", seriesCount, category.Name) } + log.Printf("[iptv-proxy] Total tracks in playlist: %d", len(playlist.Tracks)) return playlist, nil } @@ -188,6 +280,7 @@ func (c *Config) xtreamGet(ctx *gin.Context) { } func (c *Config) xtreamApiGet(ctx *gin.Context) { + log.Printf("[iptv-proxy] xtreamApiGet called") const ( apiGet = "apiget" ) @@ -196,6 +289,7 @@ func (c *Config) xtreamApiGet(ctx *gin.Context) { extension = ctx.Query("output") cacheName = apiGet + extension ) + log.Printf("[iptv-proxy] Extension: %s, CacheName: %s", extension, cacheName) xtreamM3uCacheLock.RLock() meta, ok := xtreamM3uCache[cacheName] diff --git a/pkg/xtream-proxy/xtream-proxy.go b/pkg/xtream-proxy/xtream-proxy.go index 1e88337f..2d60f601 100644 --- a/pkg/xtream-proxy/xtream-proxy.go +++ b/pkg/xtream-proxy/xtream-proxy.go @@ -20,7 +20,10 @@ package xtreamproxy import ( "context" + "encoding/json" "fmt" + "io/ioutil" + "log" "net/http" "net/url" "strconv" @@ -95,6 +98,7 @@ func (c *Client) login(proxyUser, proxyPassword, proxyURL string, proxyPort int, // Action execute an xtream action. func (c *Client) Action(config *config.ProxyConfig, action string, q url.Values) (respBody interface{}, httpcode int, err error) { + log.Printf("[xtream-proxy] Action called: '%s' with params: %v", action, q) protocol := "http" if config.HTTPS { protocol = "https" @@ -124,13 +128,220 @@ func (c *Client) Action(config *config.ProxyConfig, action string, q url.Values) } respBody, err = c.GetVideoOnDemandInfo(q["vod_id"][0]) case getSeriesCategories: + log.Printf("[xtream-proxy] Getting series categories...") respBody, err = c.GetSeriesCategories() + if err == nil { + if categories, ok := respBody.([]xtream.Category); ok { + log.Printf("[xtream-proxy] Found %d series categories", len(categories)) + } + } case getSeries: categoryID := "" if len(q["category_id"]) > 0 { categoryID = q["category_id"][0] } - respBody, err = c.GetSeries(categoryID) + log.Printf("[xtream-proxy] Getting series for category: '%s'", categoryID) + + // If no category_id is provided, get series from all categories + if categoryID == "" { + log.Printf("[xtream-proxy] No category specified, trying to get all series using raw HTTP call...") + + // Try to get all series using raw HTTP call to bypass parsing issues + originalURL := fmt.Sprintf("%s/player_api.php?username=%s&password=%s&action=get_series", + c.XtreamClient.BaseURL, c.XtreamClient.Username, c.XtreamClient.Password) + + resp, err := http.Get(originalURL) + if err != nil { + log.Printf("[xtream-proxy] Error calling original server: %v", err) + } else { + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + // Read raw response + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("[xtream-proxy] Error reading response body: %v", err) + } else { + // Try to parse as raw JSON with more tolerance + var rawSeries []map[string]interface{} + err = json.Unmarshal(body, &rawSeries) + if err != nil { + log.Printf("[xtream-proxy] Error parsing raw JSON: %v", err) + } else { + log.Printf("[xtream-proxy] Successfully parsed %d series from raw response", len(rawSeries)) + + // Convert raw data to SeriesInfo structs with error tolerance + var convertedSeries []xtream.SeriesInfo + for _, rawSerie := range rawSeries { + serie := xtream.SeriesInfo{} + + // Safely extract fields with fallbacks + if name, ok := rawSerie["name"].(string); ok { + serie.Name = name + } + if cover, ok := rawSerie["cover"].(string); ok { + serie.Cover = cover + } + if seriesID, ok := rawSerie["series_id"]; ok { + switch v := seriesID.(type) { + case float64: + serie.SeriesID = xtream.FlexInt(int(v)) + case string: + if v != "" { + if id, err := strconv.Atoi(v); err == nil { + serie.SeriesID = xtream.FlexInt(id) + } + } + } + } + + // Extract category_id to preserve category information + if categoryID, ok := rawSerie["category_id"]; ok { + switch v := categoryID.(type) { + case float64: + flexInt := xtream.FlexInt(int(v)) + serie.CategoryID = &flexInt + case string: + if v != "" { + if id, err := strconv.Atoi(v); err == nil { + flexInt := xtream.FlexInt(id) + serie.CategoryID = &flexInt + } + } + } + } + + // Extract other important fields + if plot, ok := rawSerie["plot"].(string); ok { + serie.Plot = plot + } + if cast, ok := rawSerie["cast"].(string); ok { + serie.Cast = cast + } + if director, ok := rawSerie["director"].(string); ok { + serie.Director = director + } + if genre, ok := rawSerie["genre"].(string); ok { + serie.Genre = genre + } + if releaseDate, ok := rawSerie["releaseDate"].(string); ok { + serie.ReleaseDate = releaseDate + } + if rating, ok := rawSerie["rating"]; ok { + if ratingStr, ok := rating.(string); ok { + if ratingInt, err := strconv.Atoi(ratingStr); err == nil { + serie.Rating = xtream.FlexInt(ratingInt) + } + } else if ratingFloat, ok := rating.(float64); ok { + serie.Rating = xtream.FlexInt(int(ratingFloat)) + } + } + + convertedSeries = append(convertedSeries, serie) + } + + log.Printf("[xtream-proxy] Successfully converted %d series", len(convertedSeries)) + respBody = convertedSeries + return respBody, 0, nil + } + } + } else { + log.Printf("[xtream-proxy] Original server returned status: %d", resp.StatusCode) + } + } + + // Fallback to our category-by-category approach if original server fails + log.Printf("[xtream-proxy] Original server approach failed, falling back to category-by-category...") + categories, err := c.GetSeriesCategories() + if err != nil { + log.Printf("[xtream-proxy] Error getting series categories: %v", err) + return nil, http.StatusInternalServerError, err + } + + var allSeries []xtream.SeriesInfo + successCount := 0 + errorCount := 0 + + for _, category := range categories { + categorySeries, err := c.GetSeries(fmt.Sprint(category.ID)) + if err != nil { + errorCount++ + log.Printf("[xtream-proxy] Error getting series for category %d (%s): %v", category.ID, category.Name, err) + // Continue with next category instead of failing completely + continue + } + if len(categorySeries) > 0 { + allSeries = append(allSeries, categorySeries...) + successCount++ + log.Printf("[xtream-proxy] Added %d series from category: %s", len(categorySeries), category.Name) + } else { + log.Printf("[xtream-proxy] No series found in category: %s", category.Name) + } + } + log.Printf("[xtream-proxy] Series loading complete: %d categories successful, %d failed, %d total series", successCount, errorCount, len(allSeries)) + respBody = allSeries + } else { + // Category specified, try to get series for that specific category + log.Printf("[xtream-proxy] Getting series for specific category: %s", categoryID) + respBody, err = c.GetSeries(categoryID) + if err != nil { + log.Printf("[xtream-proxy] Error getting series for category %s: %v", categoryID, err) + // If specific category fails, try to filter from all series + log.Printf("[xtream-proxy] Trying to filter from all series...") + + allSeriesURL := fmt.Sprintf("%s/player_api.php?username=%s&password=%s&action=get_series", + c.XtreamClient.BaseURL, c.XtreamClient.Username, c.XtreamClient.Password) + + resp, err := http.Get(allSeriesURL) + if err == nil { + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err == nil { + var rawSeries []map[string]interface{} + err = json.Unmarshal(body, &rawSeries) + if err == nil { + var filteredSeries []xtream.SeriesInfo + for _, rawSerie := range rawSeries { + if catID, ok := rawSerie["category_id"]; ok { + catIDStr := fmt.Sprintf("%v", catID) + if catIDStr == categoryID { + serie := xtream.SeriesInfo{} + if name, ok := rawSerie["name"].(string); ok { + serie.Name = name + } + if cover, ok := rawSerie["cover"].(string); ok { + serie.Cover = cover + } + if seriesID, ok := rawSerie["series_id"]; ok { + switch v := seriesID.(type) { + case float64: + serie.SeriesID = xtream.FlexInt(int(v)) + case string: + if v != "" { + if id, err := strconv.Atoi(v); err == nil { + serie.SeriesID = xtream.FlexInt(id) + } + } + } + } + filteredSeries = append(filteredSeries, serie) + } + } + } + log.Printf("[xtream-proxy] Filtered %d series for category %s", len(filteredSeries), categoryID) + respBody = filteredSeries + err = nil + } + } + } + } + } else { + if series, ok := respBody.([]xtream.SeriesInfo); ok { + log.Printf("[xtream-proxy] Found %d series in category %s", len(series), categoryID) + } + } + } case getSerieInfo: httpcode, err = validateParams(q, "series_id") if err != nil { @@ -144,9 +355,10 @@ func (c *Client) Action(config *config.ProxyConfig, action string, q url.Values) if err != nil { return } - if len(q["limit"]) > 0 { + if len(q["limit"]) > 0 && q["limit"][0] != "" { limit, err = strconv.Atoi(q["limit"][0]) if err != nil { + log.Printf("[xtream-proxy] Error parsing limit '%s': %v", q["limit"][0], err) httpcode = http.StatusInternalServerError return }