demo/live
demo/main.exe
*.m4s
-*.m3u8
demo/build.bat
demo/build.sh
Reply/0.flv.log
import (
"context"
+ "iter"
"net/http"
"time"
_ "github.com/qydysky/bili_danmu/Reply/F/danmuCountPerMin"
_ "github.com/qydysky/bili_danmu/Reply/F/danmuEmotes"
_ "github.com/qydysky/bili_danmu/Reply/F/danmuji"
+ _ "github.com/qydysky/bili_danmu/Reply/F/parseM3u8"
_ "github.com/qydysky/bili_danmu/Reply/F/videoFastSeed"
comp "github.com/qydysky/part/component2"
log "github.com/qydysky/part/log"
InitSav(fastSeedFilePath string) (savIndex func(seedTo time.Duration, cuIndex int64) error, e error)
}](`videoFastSeed`)
+var ParseM3u8 = comp.Get[interface {
+ Parse(respon []byte, lastNo int) (m4sLink iter.Seq[interface {
+ IsHeader() bool
+ M4sLink() string
+ }], redirectUrl string, err error)
+ IsErrRedirect(e error) bool
+}](`parseM3u8`)
+
type DanmuEmotesS struct {
Logg *log.Log_interface
Info []any
--- /dev/null
+package parsem3u8
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "iter"
+ "strconv"
+ "strings"
+
+ comp "github.com/qydysky/part/component2"
+)
+
+type TargetInterface interface {
+ Parse(respon []byte, lastNo int) (m4sLink iter.Seq[interface {
+ IsHeader() bool
+ M4sLink() string
+ }], redirectUrl string, err error)
+ IsErrRedirect(e error) bool
+}
+
+func init() {
+ if e := comp.Register[TargetInterface]("parseM3u8", parseM3u8{}); e != nil {
+ panic(e)
+ }
+}
+
+var (
+ ErrRedirect = errors.New(`ErrRedirect`)
+
+ extXStreamInf = []byte("#EXT-X-STREAM-INF")
+ extXMap = []byte("#EXT-X-MAP")
+)
+
+type parseM3u8I struct {
+ link string
+ header bool
+}
+
+func (t parseM3u8I) IsHeader() bool { return t.header }
+func (t parseM3u8I) M4sLink() string { return t.link }
+
+type parseM3u8 struct{}
+
+func (t parseM3u8) IsErrRedirect(e error) bool {
+ return e != nil && errors.Is(e, ErrRedirect)
+}
+
+func (t parseM3u8) Parse(respon []byte, lastNo int) (m4sLink iter.Seq[interface {
+ IsHeader() bool
+ M4sLink() string
+}], redirectUrl string, err error) {
+ // base64解码
+ if len(respon) != 0 && !bytes.Contains(respon, []byte("#")) {
+ respon, err = base64.StdEncoding.DecodeString(string(respon))
+ if err != nil {
+ return
+ }
+ }
+
+ m3u := bytes.Split(respon, []byte("\n"))
+ var maxqn int = -1
+ for i := 0; i < len(m3u); i++ {
+ if bytes.HasPrefix(m3u[i], extXStreamInf) {
+ // m3u8 指向新连接
+ tmp := strings.TrimSpace(string(m3u[i+1]))
+ if redirectUrl == "" {
+ redirectUrl = tmp
+ }
+ if qn, e := strconv.Atoi(ParseQuery(tmp, "qn=")); e == nil {
+ if maxqn < qn {
+ maxqn = qn
+ redirectUrl = tmp
+ }
+ }
+ err = ErrRedirect
+ }
+ }
+ if t.IsErrRedirect(err) {
+ return
+ }
+
+ m4sLink = func(yield func(interface {
+ IsHeader() bool
+ M4sLink() string
+ }) bool) {
+ for i := 0; i < len(m3u); i++ {
+ line := m3u[i]
+ if len(line) == 0 {
+ continue
+ }
+
+ var (
+ m4sLink string //切片文件名
+ isHeader bool
+ )
+
+ if line[0] == '#' {
+ if bytes.HasPrefix(line, extXMap) {
+ if lastNo != 0 {
+ continue
+ }
+ e := bytes.Index(line[16:], []byte(`"`)) + 16
+ m4sLink = string(line[16:e])
+ isHeader = true
+ } else {
+ continue
+ }
+ } else {
+ m4sLink = string(line)
+ }
+
+ if !isHeader {
+ // 只增加新的切片
+ if no, _ := strconv.Atoi(m4sLink[:len(m4sLink)-4]); lastNo >= no {
+ continue
+ }
+ }
+
+ if !yield(parseM3u8I{
+ header: isHeader,
+ link: m4sLink,
+ }) {
+ break
+ }
+ }
+ }
+ return
+}
+
+// just faster, use in right way
+//
+// eg. ParseQuery(`http://1.com/2?workspace=1`, "workspace=") => `1`
+func ParseQuery(rawURL, key string) string {
+ s := 0
+ for i := 0; i < len(rawURL); i++ {
+ if rawURL[i] == '?' {
+ s = i + 1
+ break
+ }
+ }
+
+ for i := s; i < len(rawURL); i++ {
+ for j := 0; i < len(rawURL) && j < len(key); j, i = j+1, i+1 {
+ if rawURL[i] != key[j] {
+ break
+ } else if j == len(key)-1 {
+ s = i + 1
+ i = len(rawURL)
+ break
+ }
+ }
+ }
+
+ d := s
+ for ; d < len(rawURL); d++ {
+ if rawURL[d] == '&' || rawURL[d] == '#' {
+ break
+ }
+ }
+
+ return rawURL[s:d]
+}
--- /dev/null
+package parsem3u8
+
+import (
+ "iter"
+ "testing"
+
+ _ "embed"
+
+ comp "github.com/qydysky/part/component2"
+)
+
+//go:embed test.m3u8
+var testM3u8 []byte
+
+func TestMain(t *testing.T) {
+ var ParseM3u8 = comp.Get[interface {
+ Parse(respon []byte, lastNo int) (m4sLink iter.Seq[interface {
+ IsHeader() bool
+ M4sLink() string
+ }], redirectUrl string, err error)
+ IsErrRedirect(e error) bool
+ }](`parseM3u8`)
+ _, url, e := ParseM3u8.Parse(testM3u8, 0)
+ if !ParseM3u8.IsErrRedirect(e) {
+ t.Fatal()
+ }
+ if url != "https://d1--cn-gotcha209.bilivideo.com/live-bvc/192693/live_194484313_8775758_bluray/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=10000&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha209&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&deploy_env=prod&hot_cdn=909773&origin_bitrate=1272698&source=puv3_master&flvsk=x&suffix=bluray&pp=rtmp&qp=bqor_250&sl=10&p2p_type=1&sk=x&score=42&info_source=cache&vd=bc&src=puv3&order=1" {
+ t.Fatal()
+ }
+}
--- /dev/null
+#EXTM3U
+#EXT-X-CONTENT-STEERING:SERVER-URI="reserved",PATHWAY-ID="https://d1--cn-gotcha209.bilivideo.com",BILI-PATHWAY-PRIORITY="https://d1--cn-gotcha209.bilivideo.com,https://cn-hnzz-cm-01-11.bilivideo.com,https://d1--cn-gotcha204.bilivideo.com,https://d1--cn-gotcha208.bilivideo.com,https://cn-zjhz-cm-01-17.bilivideo.com,https://d1--cn-gotcha204-1.bilivideo.com,https://cn-hnzz-cm-01-09.bilivideo.com,https://d1--cn-gotcha204-4.bilivideo.com",BILI-CTR-MODE=1,BILI-TTL=150
+#EXT-X-STREAM-INF:BANDWIDTH=1187465,RESOLUTION=1280x720,CODECS="avc1.640028,mp4a.40.2",BILI-ORDER=0,BILI-DISPLAY="超清",BILI-QN=250,PATHWAY-ID="https://d1--cn-gotcha209.bilivideo.com"
+https://d1--cn-gotcha209.bilivideo.com/live-bvc/833265/live_194484313_8775758_2500/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=250&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha209&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&p2p_type=1&sl=10&qp=bqor_250&source=puv3_master&sk=x&deploy_env=prod&score=37&hot_cdn=909773&info_source=cache&suffix=2500&origin_bitrate=1272698&flvsk=x&pp=rtmp&vd=bc&src=puv3&order=1
+#EXT-X-STREAM-INF:BANDWIDTH=1187465,RESOLUTION=1280x720,CODECS="avc1.640028,mp4a.40.2",BILI-ORDER=0,BILI-DISPLAY="超清",BILI-QN=250,PATHWAY-ID="https://cn-hnzz-cm-01-11.bilivideo.com"
+https://cn-hnzz-cm-01-11.bilivideo.com/live-bvc/833265/live_194484313_8775758_2500/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=250&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha01&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&sid=cn-hnzz-cm-01-11&chash=1&bmt=1&sg=lr&trace=65&isp=cm&rg=South&pv=Guangdong&p2p_type=1&sl=10&qp=bqor_250&source=puv3_master&deploy_env=prod&score=37&hot_cdn=909773&info_source=cache&suffix=2500&origin_bitrate=1272698&flvsk=x&sk=x&pp=rtmp&vd=nc&zoneid_l=151371779&sid_l=live_194484313_8775758_2500&src=puv3&order=2
+#EXT-X-STREAM-INF:BANDWIDTH=1187465,RESOLUTION=1280x720,CODECS="avc1.640028,mp4a.40.2",BILI-ORDER=0,BILI-DISPLAY="超清",BILI-QN=250,PATHWAY-ID="https://d1--cn-gotcha204.bilivideo.com"
+https://d1--cn-gotcha204.bilivideo.com/live-bvc/833265/live_194484313_8775758_2500/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=250&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha204&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&p2p_type=1&sl=10&qp=bqor_250&source=puv3_master&deploy_env=prod&score=37&hot_cdn=909773&info_source=cache&suffix=2500&origin_bitrate=1272698&flvsk=x&sk=x&pp=rtmp&vd=bc&src=puv3&order=3
+#EXT-X-STREAM-INF:BANDWIDTH=1187465,RESOLUTION=1280x720,CODECS="avc1.640028,mp4a.40.2",BILI-ORDER=0,BILI-DISPLAY="超清",BILI-QN=250,PATHWAY-ID="https://d1--cn-gotcha208.bilivideo.com"
+https://d1--cn-gotcha208.bilivideo.com/live-bvc/833265/live_194484313_8775758_2500/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=250&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha208&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&flvsk=x&sk=x&deploy_env=prod&score=37&hot_cdn=909773&info_source=cache&suffix=2500&origin_bitrate=1272698&pp=rtmp&source=puv3_master&p2p_type=1&sl=10&qp=bqor_250&vd=bc&src=puv3&order=4
+#EXT-X-STREAM-INF:BANDWIDTH=1724079,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=1,BILI-DISPLAY="蓝光",BILI-QN=400,PATHWAY-ID="https://d1--cn-gotcha209.bilivideo.com"
+https://d1--cn-gotcha209.bilivideo.com/live-bvc/378106/live_194484313_8775758_4000/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=400&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha209&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&sl=10&qp=bqor_250&flvsk=x&deploy_env=prod&suffix=4000&p2p_type=1&score=57&origin_bitrate=1272698&source=puv3_master&sk=x&info_source=cache&hot_cdn=909773&pp=rtmp&vd=bc&src=puv3&order=1
+#EXT-X-STREAM-INF:BANDWIDTH=1724079,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=1,BILI-DISPLAY="蓝光",BILI-QN=400,PATHWAY-ID="https://cn-zjhz-cm-01-17.bilivideo.com"
+https://cn-zjhz-cm-01-17.bilivideo.com/live-bvc/378106/live_194484313_8775758_4000/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=400&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha01&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&sid=cn-zjhz-cm-01-17&chash=1&bmt=1&sg=lr&trace=65&isp=cm&rg=South&pv=Guangdong&deploy_env=prod&suffix=4000&p2p_type=1&sl=10&qp=bqor_250&flvsk=x&sk=x&info_source=cache&hot_cdn=909773&pp=rtmp&score=57&origin_bitrate=1272698&source=puv3_master&vd=nc&zoneid_l=151371779&sid_l=live_194484313_8775758_4000&src=puv3&order=2
+#EXT-X-STREAM-INF:BANDWIDTH=1724079,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=1,BILI-DISPLAY="蓝光",BILI-QN=400,PATHWAY-ID="https://d1--cn-gotcha204-1.bilivideo.com"
+https://d1--cn-gotcha204-1.bilivideo.com/live-bvc/378106/live_194484313_8775758_4000/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=400&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha204&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&sk=x&info_source=cache&hot_cdn=909773&pp=rtmp&score=57&origin_bitrate=1272698&source=puv3_master&deploy_env=prod&suffix=4000&p2p_type=1&sl=10&qp=bqor_250&flvsk=x&vd=bc&src=puv3&order=3
+#EXT-X-STREAM-INF:BANDWIDTH=1724079,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=1,BILI-DISPLAY="蓝光",BILI-QN=400,PATHWAY-ID="https://d1--cn-gotcha208.bilivideo.com"
+https://d1--cn-gotcha208.bilivideo.com/live-bvc/378106/live_194484313_8775758_4000/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=400&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha208&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&sl=10&qp=bqor_250&flvsk=x&deploy_env=prod&suffix=4000&p2p_type=1&score=57&origin_bitrate=1272698&source=puv3_master&sk=x&info_source=cache&hot_cdn=909773&pp=rtmp&vd=bc&src=puv3&order=4
+#EXT-X-STREAM-INF:BANDWIDTH=2108696,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=2,BILI-DISPLAY="原画",BILI-QN=10000,PATHWAY-ID="https://d1--cn-gotcha209.bilivideo.com"
+https://d1--cn-gotcha209.bilivideo.com/live-bvc/192693/live_194484313_8775758_bluray/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=10000&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha209&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&deploy_env=prod&hot_cdn=909773&origin_bitrate=1272698&source=puv3_master&flvsk=x&suffix=bluray&pp=rtmp&qp=bqor_250&sl=10&p2p_type=1&sk=x&score=42&info_source=cache&vd=bc&src=puv3&order=1
+#EXT-X-STREAM-INF:BANDWIDTH=2108696,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=2,BILI-DISPLAY="原画",BILI-QN=10000,PATHWAY-ID="https://cn-hnzz-cm-01-09.bilivideo.com"
+https://cn-hnzz-cm-01-09.bilivideo.com/live-bvc/192693/live_194484313_8775758_bluray/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=10000&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha01&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&sid=cn-hnzz-cm-01-09&chash=1&bmt=1&sg=lr&trace=65&isp=cm&rg=South&pv=Guangdong&flvsk=x&suffix=bluray&pp=rtmp&qp=bqor_250&deploy_env=prod&hot_cdn=909773&origin_bitrate=1272698&source=puv3_master&info_source=cache&sl=10&p2p_type=1&sk=x&score=42&vd=nc&zoneid_l=151371779&sid_l=live_194484313_8775758_bluray&src=puv3&order=2
+#EXT-X-STREAM-INF:BANDWIDTH=2108696,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=2,BILI-DISPLAY="原画",BILI-QN=10000,PATHWAY-ID="https://d1--cn-gotcha204-4.bilivideo.com"
+https://d1--cn-gotcha204-4.bilivideo.com/live-bvc/192693/live_194484313_8775758_bluray/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=10000&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha204&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&sl=10&p2p_type=1&sk=x&score=42&info_source=cache&pp=rtmp&qp=bqor_250&deploy_env=prod&hot_cdn=909773&origin_bitrate=1272698&source=puv3_master&flvsk=x&suffix=bluray&vd=bc&src=puv3&order=3
+#EXT-X-STREAM-INF:BANDWIDTH=2108696,RESOLUTION=1920x1080,CODECS="avc1.640032,mp4a.40.2",BILI-ORDER=2,BILI-DISPLAY="原画",BILI-QN=10000,PATHWAY-ID="https://d1--cn-gotcha208.bilivideo.com"
+https://d1--cn-gotcha208.bilivideo.com/live-bvc/192693/live_194484313_8775758_bluray/index.m3u8?expires=1737129387&len=0&oi=x&pt=web&qn=10000&trid=x&sigparams=cdn,expires,len,oi,pt,qn,trid&cdn=cn-gotcha208&sign=x&site=x&free_type=0&mid=x&sche=ban&bvchls=1&trace=64&isp=cm&rg=South&pv=Guangdong&qp=bqor_250&deploy_env=prod&hot_cdn=909773&origin_bitrate=1272698&source=puv3_master&flvsk=x&suffix=bluray&pp=rtmp&sl=10&p2p_type=1&sk=x&score=42&info_source=cache&vd=bc&src=puv3&order=4
\ No newline at end of file
package reply
import (
- "bytes"
"context"
"crypto/md5"
_ "embed"
- "encoding/base64"
"encoding/json"
"errors"
"fmt"
defer t.reqPool.Put(r)
// 请求解析m3u8内容
- for _, v := range t.common.Live {
+ for i := 0; i < len(t.common.Live); i++ {
+ v := t.common.Live[i]
+
// 跳过尚未启用的live地址
if !v.Valid() {
continue
}
if err := r.Reqf(rval); err != nil {
- // 1min后重新启用
v.DisableAuto()
t.log.L("W: ", fmt.Sprintf("服务器 %s 发生故障 %s", F.ParseHost(v.Url), pe.ErrorFormat(err, pe.ErrSimplifyFunc)))
if t.common.ValidLive() == nil {
}
}
- // m3u8字节流
- var m3u8_respon = r.Respon
-
- // base64解码
- if len(m3u8_respon) != 0 && !bytes.Contains(m3u8_respon, []byte("#")) {
- var err error
- m3u8_respon, err = base64.StdEncoding.DecodeString(string(m3u8_respon))
- if err != nil {
- e = err
- return
- }
- }
-
// 解析m3u8
// var tmp []*m4s_link_item
var lastNo int
lastNo, _ = lastM4s.getNo()
}
- m3u := bytes.Split(m3u8_respon, []byte("\n"))
- for i := 0; i < len(m3u); i++ {
- line := m3u[i]
- if len(line) == 0 {
- continue
- }
-
- var (
- m4s_link string //切片文件名
- isHeader bool
- )
-
- if line[0] == '#' {
- if strings.HasPrefix(string(line), "#EXT-X-MAP") {
- if lastM4s != nil {
- continue
- }
- e := bytes.Index(line[16:], []byte(`"`)) + 16
- m4s_link = string(line[16:e])
- isHeader = true
- } else if strings.HasPrefix(string(line), "#EXT-X-STREAM-INF") {
- // m3u8 指向新连接
- i += 1
- line = m3u[i]
- newUrl := strings.TrimSpace(string(line))
- t.log.L(`I: `, `指向新连接`, v.Host(), "=>", F.ParseHost(newUrl))
- v.SetUrl(newUrl)
- continue
- } else {
- continue
- }
+ if rg, redirectUrl, err := replyFunc.ParseM3u8.Parse(r.Respon, lastNo); err != nil {
+ if replyFunc.ParseM3u8.IsErrRedirect(err) {
+ // 指向新连接
+ t.log.L(`I: `, `指向新连接`, v.Host(), "=>", F.ParseHost(redirectUrl))
+ v.SetUrl(redirectUrl)
+ i -= 1
} else {
- m4s_link = string(line)
- }
-
- if !isHeader {
- // 只增加新的切片
- if no, _ := strconv.Atoi(m4s_link[:len(m4s_link)-4]); lastNo >= no {
- continue
+ // 1min后重新启用
+ t.log.L("W: ", fmt.Sprintf("服务器 %s 发生故障 %v", F.ParseHost(v.Url), err))
+ v.DisableAuto()
+ if t.common.ValidLive() == nil {
+ e = errors.New("全部切片服务器发生故障")
+ break
}
}
-
- //将切片添加到返回切片数组
- p := t.getM4s()
- p.SerUuid = v.Uuid
- p.Url = F.ResolveReferenceLast(v.Url, m4s_link+"?trid="+F.ParseQuery(v.Url, "trid="))
- p.Base = m4s_link
- p.isHeader = isHeader
- p.createdTime = time.Now()
- m4s_links = append(m4s_links, p)
+ continue
+ } else {
+ for m4sLinkI := range rg {
+ //将切片添加到返回切片数组
+ p := t.getM4s()
+ p.SerUuid = v.Uuid
+ p.Url = F.ResolveReferenceLast(v.Url, m4sLinkI.M4sLink()+"?trid="+F.ParseQuery(v.Url, "trid="))
+ p.Base = m4sLinkI.M4sLink()
+ p.isHeader = m4sLinkI.IsHeader()
+ p.createdTime = time.Now()
+ m4s_links = append(m4s_links, p)
+ }
}
if len(m4s_links) == 0 {