diff --git a/README.md b/README.md index 7401d53..a10285c 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ IPTV工具,功能列表如下: } ``` -| 字段 | 说明 | -|-------------------|--------------------------------------------------------------------------------------------------------------------------| -| key | 不是Authenticator,而是生成Authenticator的秘钥,每个IPTV机顶盒可能都不同,可通过工具根据某次抓包获取的Authenticator反向破解key,具体见下面的使用介绍。 | -| interfaceName | 设备的网络接口名称,和ip字段二选一,优先使用该字段的值。当工具运行在软路由上时,可通过配置自动获取指定接口的IPv4地址。用于获取软路由上某接口被自动分配的IPTV线路的IP地址。 | -| serverHost | 工具请求的IPTV服务器地址,注意需要走IPTV专用网络才能访问通。 | -| ip | 客户端的ip,可任意配置,生成Authenticator所需。当interfaceName已配置时,优先通过interfaceName获取。 | -| channelProgramAPI | 请求频道节目信息的API接口,目前只支持两种:`liveplay_30`或`gdhdpublic`,缺省为`liveplay_30`。 | +| 字段 | 说明 | +|-------------------|------------------------------------------------------------------------------------------------------| +| key | 不是Authenticator,而是生成Authenticator的秘钥,每个IPTV机顶盒可能都不同,可通过工具根据某次抓包获取的Authenticator反向破解key,具体见下面的使用介绍。 | +| interfaceName | 设备的网络接口名称,和ip字段二选一,优先使用该字段的值。当工具运行在软路由上时,可通过配置自动获取指定接口的IPv4地址。用于获取软路由上某接口被自动分配的IPTV线路的IP地址。 | +| serverHost | 工具请求的IPTV服务器地址,注意需要走IPTV专用网络才能访问通。 | +| ip | 客户端的ip,可任意配置,生成Authenticator所需。当interfaceName已配置时,优先通过interfaceName获取。 | +| channelProgramAPI | 请求频道节目信息的API接口,目前只支持两种:`liveplay_30`或`gdhdpublic`。
未配置时,工具将自动进行尝试。 | | 其他字段 | 均可通过抓包获取(注意x-requested-with可通过抓包HTTP请求头拿到)。 必填字段:userID,stbType,stbVersion,stbID,mac,softwareVersion | ### 使用介绍 @@ -98,10 +98,10 @@ http://IP:PORT/channel/m3u?csFormat={format}&multiFirst={multiFirst} 1. 参数csFormat可指定回看catchup-source的请求格式,非必填。可选值如下: - | 值 | 是否缺省 | 说明 | - |---|------|-------------------------------------------------------| - | 0 | 是 | `?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}` | - | 1 | 否 | `?playseek={utc:YmdHMS}-{utcend:YmdHMS}` | + | 值 | 是否缺省 | 说明 | + |---|------|-------------------------------------------------------| + | 0 | 是 | `?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}` | + | 1 | 否 | `?playseek={utc:YmdHMS}-{utcend:YmdHMS}` | 2. 参数multiFirst:当频道存在多个URL地址时,是否优先使用组播地址。可选值:`true`或`false`。非必填,缺省为`true`。 diff --git a/internal/app/iptv/ct/epg.go b/internal/app/iptv/ct/epg.go index d0c6a02..4db3235 100644 --- a/internal/app/iptv/ct/epg.go +++ b/internal/app/iptv/ct/epg.go @@ -4,10 +4,15 @@ import ( "context" "errors" "iptv/internal/app/iptv" + + "go.uber.org/zap" ) -var ErrParseChProgList = errors.New("failed to parse channel program list") -var ErrChProgListIsEmpty = errors.New("the list of programs is empty") +var ( + ErrParseChProgList = errors.New("failed to parse channel program list") + ErrChProgListIsEmpty = errors.New("the list of programs is empty") + ErrEPGApiNotFound = errors.New("epg api not found") +) const ( chProgAPILiveplay = "liveplay_30" @@ -36,10 +41,15 @@ func (c *Client) GetAllChannelProgramList(ctx context.Context, channels []iptv.C case chProgAPIGdhdpublic: progList, err = c.getGdhdpublicChannelProgramList(ctx, token, &channel) default: - progList, err = c.getLiveplayChannelProgramList(ctx, token, &channel) + // 自动选择调用EPG的API接口 + progList, err = c.getChannelProgramListByAuto(ctx, token, &channel) } if err != nil { + if errors.Is(err, ErrEPGApiNotFound) { + c.logger.Error("Failed to get channel program list.", zap.Error(err)) + break + } c.logger.Sugar().Warnf("Failed to get the program list for channel %s. Error: %v", channel.ChannelName, err) continue } @@ -49,3 +59,22 @@ func (c *Client) GetAllChannelProgramList(ctx context.Context, channels []iptv.C return epg, nil } + +// getChannelProgramListByAuto 自动选择调用EPG的API接口 +func (c *Client) getChannelProgramListByAuto(ctx context.Context, token *Token, channel *iptv.Channel) (*iptv.ChannelProgramList, error) { + progList, err := c.getLiveplayChannelProgramList(ctx, token, channel) + if !errors.Is(err, ErrEPGApiNotFound) { + c.logger.Info("An available EPG API was found.", zap.String("channelProgramAPI", chProgAPILiveplay)) + c.config.ChannelProgramAPI = chProgAPILiveplay + return progList, err + } + + progList, err = c.getGdhdpublicChannelProgramList(ctx, token, channel) + if !errors.Is(err, ErrEPGApiNotFound) { + c.logger.Info("An available EPG API was found.", zap.String("channelProgramAPI", chProgAPIGdhdpublic)) + c.config.ChannelProgramAPI = chProgAPIGdhdpublic + return progList, err + } + + return nil, err +} diff --git a/internal/app/iptv/ct/epg_gdhdpublic.go b/internal/app/iptv/ct/epg_gdhdpublic.go index b659d2a..d9e0526 100644 --- a/internal/app/iptv/ct/epg_gdhdpublic.go +++ b/internal/app/iptv/ct/epg_gdhdpublic.go @@ -3,6 +3,7 @@ package ct import ( "context" "encoding/json" + "errors" "fmt" "io" "iptv/internal/app/iptv" @@ -47,6 +48,9 @@ func (c *Client) getGdhdpublicChannelProgramList(ctx context.Context, token *Tok // 获取指定日期的节目单列表 dateProgram, err := c.getGdhdpublicChannelDateProgram(ctx, token, channel.ChannelID, dateStr) if err != nil { + if errors.Is(err, ErrEPGApiNotFound) { + return nil, err + } c.logger.Sugar().Warnf("Failed to get the program list for channel %s on %s. Error: %v", channel.ChannelName, dateStr, err) continue } @@ -97,7 +101,9 @@ func (c *Client) getGdhdpublicChannelDateProgram(ctx context.Context, token *Tok } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, ErrEPGApiNotFound + } else if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status code: %d", resp.StatusCode) } diff --git a/internal/app/iptv/ct/epg_liveplay.go b/internal/app/iptv/ct/epg_liveplay.go index ca16384..e5ce178 100644 --- a/internal/app/iptv/ct/epg_liveplay.go +++ b/internal/app/iptv/ct/epg_liveplay.go @@ -41,7 +41,9 @@ func (c *Client) getLiveplayChannelProgramList(ctx context.Context, token *Token } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, ErrEPGApiNotFound + } else if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status code: %d", resp.StatusCode) } @@ -59,7 +61,7 @@ func (c *Client) getLiveplayChannelProgramList(ctx context.Context, token *Token } // 解析节目单 - dateProgramList, err := parseFTTHChannelProgramList(matches[1]) + dateProgramList, err := parseLiveplayChannelProgramList(matches[1]) if err != nil { return nil, err } @@ -71,8 +73,8 @@ func (c *Client) getLiveplayChannelProgramList(ctx context.Context, token *Token }, nil } -// parseFTTHChannelProgramList 解析频道节目单列表 -func parseFTTHChannelProgramList(rawData []byte) ([]iptv.DateProgram, error) { +// parseLiveplayChannelProgramList 解析频道节目单列表 +func parseLiveplayChannelProgramList(rawData []byte) ([]iptv.DateProgram, error) { // 动态解析Json var rawArray []any err := json.Unmarshal(rawData, &rawArray)