Skip to content

Commit

Permalink
feat(channel): 支持配置频道URL是否优先使用组播地址(当存在多个URL地址)。
Browse files Browse the repository at this point in the history
  • Loading branch information
super321 committed Dec 30, 2024
1 parent ed64547 commit b33f6e1
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 44 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,17 @@ Key后面的即是)。
* m3u格式直播源在线接口

```
http://IP:PORT/channel/m3u?csFormat={format}
http://IP:PORT/channel/m3u?csFormat={format}&multiFirst={multiFirst}
```

参数csFormat可指定回看catchup-source的请求格式,非必填。可选值如下:
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`

* txt格式直播源在线接口

Expand Down
6 changes: 4 additions & 2 deletions cmd/iptv/cmds/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
udpxyURL string
format string
catchupSource string
multicastFirst bool
)

func NewChannelCLI() *cobra.Command {
Expand Down Expand Up @@ -82,13 +83,13 @@ func NewChannelCLI() *cobra.Command {
switch format {
case "txt":
// 将获取到的频道列表转换为TXT格式
content, err = iptv.ToTxtFormat(channels, udpxyURL)
content, err = iptv.ToTxtFormat(channels, udpxyURL, multicastFirst)
if err != nil {
return err
}
case "m3u":
// 将获取到的频道列表转换为M3U格式
content, err = iptv.ToM3UFormat(channels, udpxyURL, catchupSource)
content, err = iptv.ToM3UFormat(channels, udpxyURL, catchupSource, multicastFirst)
if err != nil {
return err
}
Expand All @@ -109,6 +110,7 @@ func NewChannelCLI() *cobra.Command {
channelCmd.Flags().StringVarP(&udpxyURL, "udpxy", "u", "", "如果有安装udpxy进行组播转单播,请配置HTTP地址,e.g `http://192.168.1.1:4022`。")
channelCmd.Flags().StringVarP(&format, "format", "f", "m3u", "生成的直播源文件格式,e.g `m3u或txt`。")
channelCmd.Flags().StringVarP(&catchupSource, "catchup-source", "s", "?playseek=${(b)yyyyMMddHHmmss}-${(e)yyyyMMddHHmmss}", "回看的请求格式字符串,会追加在时移地址后面。")
channelCmd.Flags().BoolVarP(&multicastFirst, "multicast-first", "m", false, "当频道存在多个URL地址时,是否优先使用组播地址。")

return channelCmd
}
64 changes: 40 additions & 24 deletions internal/app/iptv/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Channel struct {
ChannelID string `json:"channelID"` // 频道ID
ChannelName string `json:"channelName"` // 频道名称
UserChannelID string `json:"userChannelID"` // 频道号
ChannelURL *url.URL `json:"channelURL"` // 频道URL
ChannelURLs []url.URL `json:"channelURLs"` // 频道URL列表
TimeShift string `json:"timeShift"` // 时移类型
TimeShiftLength time.Duration `json:"timeShiftLength"` // 支持的时移长度
TimeShiftURL *url.URL `json:"timeShiftURL"` // 时移地址(回放地址)
Expand All @@ -24,40 +24,36 @@ type Channel struct {
}

// ToM3UFormat 转换为M3U格式内容
func ToM3UFormat(channels []Channel, udpxyURL, catchupSource string) (string, error) {
func ToM3UFormat(channels []Channel, udpxyURL, catchupSource string, multicastFirst bool) (string, error) {
if len(channels) == 0 {
return "", errors.New("no channels found")
}

var sb strings.Builder
sb.WriteString("#EXTM3U\n")
for _, channel := range channels {
var err error
var channelURL string
if udpxyURL != "" && channel.ChannelURL.Scheme == SCHEME_IGMP {
channelURL, err = url.JoinPath(udpxyURL, fmt.Sprintf("/rtp/%s", channel.ChannelURL.Host))
if err != nil {
return "", err
}
} else {
channelURL = channel.ChannelURL.String()
// 根据指定条件,获取频道URL地址
channelURLStr, err := getChannelURLStr(channel.ChannelURLs, udpxyURL, multicastFirst)
if err != nil {
return "", err
}

var m3uLine string
if channel.TimeShift == "1" && channel.TimeShiftLength > 0 {
m3uLine = fmt.Sprintf("#EXTINF:-1 tvg-id=\"%s\" tvg-chno=\"%s\" catchup=\"%s\" catchup-source=\"%s\" catchup-days=\"%d\" group-title=\"%s\",%s\n%s\n",
channel.ChannelID, channel.UserChannelID, "default", channel.TimeShiftURL.String()+catchupSource,
int64(channel.TimeShiftLength.Hours()/24), channel.GroupName, channel.ChannelName, channelURL)
int64(channel.TimeShiftLength.Hours()/24), channel.GroupName, channel.ChannelName, channelURLStr)
} else {
m3uLine = fmt.Sprintf("#EXTINF:-1 tvg-id=\"%s\" tvg-chno=\"%s\" group-title=\"%s\",%s\n%s\n",
channel.ChannelID, channel.UserChannelID, channel.GroupName, channel.ChannelName, channelURL)
channel.ChannelID, channel.UserChannelID, channel.GroupName, channel.ChannelName, channelURLStr)
}
sb.WriteString(m3uLine)
}
return sb.String(), nil
}

// ToTxtFormat 转换为txt格式内容
func ToTxtFormat(channels []Channel, udpxyURL string) (string, error) {
func ToTxtFormat(channels []Channel, udpxyURL string, multicastFirst bool) (string, error) {
if len(channels) == 0 {
return "", errors.New("no channels found")
}
Expand Down Expand Up @@ -88,21 +84,41 @@ func ToTxtFormat(channels []Channel, udpxyURL string) (string, error) {

// 输出频道信息
for _, channel := range groupChannels {
var err error
var channelURL string
if udpxyURL != "" && channel.ChannelURL.Scheme == SCHEME_IGMP {
channelURL, err = url.JoinPath(udpxyURL, fmt.Sprintf("/rtp/%s", channel.ChannelURL.Host))
if err != nil {
return "", err
}
} else {
channelURL = channel.ChannelURL.String()
// 根据指定条件,获取频道URL地址
channelURLStr, err := getChannelURLStr(channel.ChannelURLs, udpxyURL, multicastFirst)
if err != nil {
return "", err
}

txtLine := fmt.Sprintf("%s,%s\n",
channel.ChannelName, channelURL)
channel.ChannelName, channelURLStr)
sb.WriteString(txtLine)
}
}
return sb.String(), nil
}

// getChannelURLStr 根据指定条件,获取频道URL地址
func getChannelURLStr(channelURLs []url.URL, udpxyURL string, multicastFirst bool) (string, error) {
if len(channelURLs) == 0 {
return "", errors.New("no channel urls found")
}

var channelURL url.URL
if len(channelURLs) == 1 {
channelURL = channelURLs[0]
} else {
for _, channelURL = range channelURLs {
if (multicastFirst && channelURL.Scheme == SCHEME_IGMP) ||
(!multicastFirst && channelURL.Scheme != SCHEME_IGMP) {
break
}
}
}

if udpxyURL != "" && channelURL.Scheme == SCHEME_IGMP {
return url.JoinPath(udpxyURL, fmt.Sprintf("/rtp/%s", channelURL.Host))
} else {
return channelURL.String(), nil
}
}
14 changes: 6 additions & 8 deletions internal/app/iptv/ct/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,19 @@ func (c *Client) GetAllChannelList(ctx context.Context) ([]iptv.Channel, error)
}

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

if channelURL != nil && channelURL.Scheme == iptv.SCHEME_IGMP {
break
}
channelURLs = append(channelURLs, *channelURL)
}

if channelURL == nil {
if len(channelURLs) == 0 {
c.logger.Warn("The channelURL of this channel is illegal, skip it.", zap.String("channelName", channelName), zap.String("channelURL", string(matches[4])))
continue
}
Expand Down Expand Up @@ -144,7 +142,7 @@ func (c *Client) GetAllChannelList(ctx context.Context) ([]iptv.Channel, error)
ChannelID: string(matches[1]),
ChannelName: channelName,
UserChannelID: string(matches[3]),
ChannelURL: channelURL,
ChannelURLs: channelURLs,
TimeShift: string(matches[5]),
TimeShiftLength: time.Duration(timeShiftLength) * time.Minute,
TimeShiftURL: timeShiftURL,
Expand Down
21 changes: 17 additions & 4 deletions internal/app/router/channel_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"iptv/internal/app/iptv"
"net/http"
"strconv"
"sync/atomic"
"time"

Expand Down Expand Up @@ -34,15 +35,21 @@ func GetM3UData(c *gin.Context) {
catchupSource = diypCatchupSource
}

channels := *channelsPtr.Load()
// 是否优先是由组播地址
multiFirstStr := c.DefaultQuery("multiFirst", "true")
multicastFirst, err := strconv.ParseBool(multiFirstStr)
if err != nil {
multicastFirst = true
}

channels := *channelsPtr.Load()
if len(channels) == 0 {
c.Status(http.StatusNotFound)
return
}

// 将获取到的频道列表转换为m3u格式
m3uContent, err := iptv.ToM3UFormat(channels, udpxyURL, catchupSource)
m3uContent, err := iptv.ToM3UFormat(channels, udpxyURL, catchupSource, multicastFirst)
if err != nil {
logger.Error("Failed to convert channel list to m3u format.", zap.Error(err))
// 返回响应
Expand All @@ -56,15 +63,21 @@ func GetM3UData(c *gin.Context) {

// GetTXTData 查询直播源txt
func GetTXTData(c *gin.Context) {
channels := *channelsPtr.Load()
// 是否优先是由组播地址
multiFirstStr := c.DefaultQuery("multiFirst", "true")
multicastFirst, err := strconv.ParseBool(multiFirstStr)
if err != nil {
multicastFirst = true
}

channels := *channelsPtr.Load()
if len(channels) == 0 {
c.Status(http.StatusNotFound)
return
}

// 将获取到的频道列表转换为txt格式
txtContent, err := iptv.ToTxtFormat(channels, udpxyURL)
txtContent, err := iptv.ToTxtFormat(channels, udpxyURL, multicastFirst)
if err != nil {
logger.Error("Failed to convert channel list to txt format.", zap.Error(err))
// 返回响应
Expand Down

0 comments on commit b33f6e1

Please sign in to comment.