Skip to content

Commit

Permalink
refactor: 重构IPTV认证、获取频道等逻辑。
Browse files Browse the repository at this point in the history
  • Loading branch information
super321 committed Dec 24, 2024
1 parent 5d9d339 commit ea72de7
Show file tree
Hide file tree
Showing 13 changed files with 428 additions and 383 deletions.
10 changes: 3 additions & 7 deletions cmd/iptv/cmds/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmds
import (
"errors"
"iptv/internal/app/iptv"
"iptv/internal/app/iptv/cdt"
"iptv/internal/pkg/util"
"net/http"
"os"
Expand Down Expand Up @@ -42,20 +43,15 @@ func NewChannelCLI() *cobra.Command {
}

// 创建IPTV客户端
i, err := iptv.NewClient(&http.Client{
i, err := cdt.NewClient(&http.Client{
Timeout: 10 * time.Second,
}, &config)
if err != nil {
return err
}

// IPTV认证
token, err := i.GenerateToken(cmd.Context())
if err != nil {
return err
}
// 获取频道列表
channels, err := i.GetChannelList(cmd.Context(), token)
channels, err := i.GetAllChannelList(cmd.Context())
if err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package iptv
package cdt

import (
"context"
"errors"
"fmt"
"io"
"iptv/internal/app/iptv"
"math/rand"
"net"
"net/http"
Expand All @@ -21,8 +22,8 @@ type Token struct {
JSESSIONID string `json:"jsessionid"`
}

// GenerateToken 认证并生成oken
func (c *Client) GenerateToken(ctx context.Context) (*Token, error) {
// requestToken 请求认证的Token
func (c *Client) requestToken(ctx context.Context) (*Token, error) {
// 访问登录页面
referer, err := c.authenticationURL(ctx, true)
if err != nil {
Expand Down Expand Up @@ -147,7 +148,7 @@ func (c *Client) validAuthenticationHWCTC(ctx context.Context, encryptToken stri
input := fmt.Sprintf("%d$%s$%s$%s$%s$%s$$CTC",
random, encryptToken, c.config.UserID, c.config.STBID, ipv4Addr, c.config.MAC)
// 使用3DES加密生成Authenticator
crypto := NewTripleDESCrypto(c.config.Key)
crypto := iptv.NewTripleDESCrypto(c.config.Key)
authenticator, err := crypto.ECBEncrypt(input)
if err != nil {
return nil, err
Expand Down
46 changes: 46 additions & 0 deletions internal/app/iptv/cdt/cdt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cdt

import (
"iptv/internal/app/iptv"
"net/http"

"go.uber.org/zap"
)

type Client struct {
httpClient *http.Client // HTTP客户端
config *iptv.Config // IPTV配置
host string // 缓存最新重定向的服务器地址和端口

logger *zap.Logger // 日志
}

var _ iptv.Client = (*Client)(nil)

func NewClient(httpClient *http.Client, config *iptv.Config) (iptv.Client, error) {
// 校验config配置
if err := config.Validate(); err != nil {
return nil, err
}

i := Client{
httpClient: httpClient,
host: config.ServerHost,
config: config,
logger: zap.L(),
}
if i.httpClient == nil {
i.httpClient = http.DefaultClient
}
return &i, nil
}

func (c *Client) setCommonHeaders(req *http.Request) {
req.Header.Set("Host", c.host)
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; Fhbw2.0) AppleWebKit/534.24 (KHTML, like Gecko) Safari/534.24 chromium/webkit")
req.Header.Set("Accept-Language", "zh-CN,en-US;q=0.8")
if c.config.XRequestedWith != "" {
req.Header.Set("X-Requested-With", c.config.XRequestedWith)
}
}
155 changes: 155 additions & 0 deletions internal/app/iptv/cdt/channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cdt

import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"iptv/internal/app/iptv"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"

"go.uber.org/zap"
)

// GetAllChannelList 获取所有频道列表
func (c *Client) GetAllChannelList(ctx context.Context) ([]iptv.Channel, error) {
// 请求认证的Token
token, err := c.requestToken(ctx)
if err != nil {
return nil, err
}

// 计算JSESSIONID的MD5
hash := md5.Sum([]byte(token.JSESSIONID))
// 转换为16进制字符串并转换为大写,即为tempKey
tempKey := hex.EncodeToString(hash[:])

// 组装请求数据
data := map[string]string{
"conntype": c.config.Conntype,
"UserToken": token.UserToken,
"tempKey": tempKey,
"stbid": token.Stbid,
"SupportHD": "1",
"UserID": c.config.UserID,
"Lang": c.config.Lang,
}
body := url.Values{}
for k, v := range data {
body.Add(k, v)
}

// 创建请求
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
fmt.Sprintf("http://%s/EPG/jsp/getchannellistHWCTC.jsp", c.host), strings.NewReader(body.Encode()))
if err != nil {
return nil, err
}

// 设置请求头
c.setCommonHeaders(req)
req.Header.Set("Referer", fmt.Sprintf("http://%s/EPG/jsp/ValidAuthenticationHWCTC.jsp", c.host))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

// 设置Cookie
req.AddCookie(&http.Cookie{
Name: "JSESSIONID",
Value: token.JSESSIONID,
})

// 执行请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status code: %d", resp.StatusCode)
}

// 解析响应内容
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
chRegex := regexp.MustCompile("ChannelID=\"(.+?)\",ChannelName=\"(.+?)\",UserChannelID=\"(.+?)\",ChannelURL=\"(.+?)\",TimeShift=\"(.+?)\",TimeShiftLength=\"(\\d+?)\".+?,TimeShiftURL=\"(.+?)\"")
matchesList := chRegex.FindAllSubmatch(result, -1)
if matchesList == nil {
return nil, fmt.Errorf("failed to extract channel list")
}

// 过滤掉特殊频道的正则表达式
chExcludeRegex := regexp.MustCompile("^.+?(画中画|单音轨)$")

channels := make([]iptv.Channel, 0, len(matchesList))
for _, matches := range matchesList {
if len(matches) != 8 {
continue
}

channelName := string(matches[2])
// 过滤掉特殊频道
if chExcludeRegex.MatchString(channelName) {
c.logger.Warn("This is not a normal channel, skip it.", zap.String("channelName", channelName))
continue
}

// channelURL类型转换
// channelURL可能同时返回组播和单播多个地址(通过|分割),这里优先取组播地址
var channelURL *url.URL
channelURLStrList := strings.Split(string(matches[4]), "|")
for _, channelURLStr := range channelURLStrList {
channelURL, err = url.Parse(channelURLStr)
if err != nil {
continue
}

if channelURL != nil && channelURL.Scheme == iptv.SCHEME_IGMP {
break
}
}

if channelURL == nil {
c.logger.Warn("The channelURL of this channel is illegal, skip it.", zap.String("channelName", channelName), zap.String("channelURL", string(matches[4])))
continue
}

// TimeShiftLength类型转换
timeShiftLength, err := strconv.ParseInt(string(matches[6]), 10, 64)
if err != nil {
c.logger.Warn("The timeShiftLength of this channel is illegal, skip it.", zap.String("channelName", channelName), zap.String("timeShiftLength", string(matches[6])))
continue
}

// 解析时移地址
timeShiftURL, err := url.Parse(string(matches[7]))
if err != nil {
c.logger.Warn("The timeShiftURL of this channel is illegal, skip it.", zap.String("channelName", channelName), zap.String("timeShiftURL", string(matches[7])))
continue
}
// 重置时移地址的查询参数
timeShiftURL.RawQuery = ""

// 自动识别频道的分类
groupName := iptv.GetChannelGroupName(channelName)

channels = append(channels, iptv.Channel{
ChannelID: string(matches[1]),
ChannelName: channelName,
UserChannelID: string(matches[3]),
ChannelURL: channelURL,
TimeShift: string(matches[5]),
TimeShiftLength: time.Duration(timeShiftLength) * time.Minute,
TimeShiftURL: timeShiftURL,
GroupName: groupName,
})
}
return channels, nil
}
Loading

0 comments on commit ea72de7

Please sign in to comment.