Skip to content

Commit

Permalink
不使用 enter api 重新实现抖音直播信息获取功能 (#549)
Browse files Browse the repository at this point in the history
* 不使用 enter api 重新实现抖音直播信息获取功能

* 修复网页端无法播放文件名中含有'#'字符的视频的问题
  • Loading branch information
kira1928 authored Sep 18, 2023
1 parent ea16aa3 commit 42cb7e3
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 46 deletions.
284 changes: 239 additions & 45 deletions src/live/douyin/douyin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"

"github.com/hr3lxphr6j/requests"
"github.com/tidwall/gjson"
Expand All @@ -17,8 +21,10 @@ const (
domain = "live.douyin.com"
cnName = "抖音"

randomCookieChars = "1234567890abcdef"
roomIdCatcherRegex = `{\\"webrid\\":\\"([^"]+)\\"}`
randomCookieChars = "1234567890abcdef"
roomIdCatcherRegex = `{\\"webrid\\":\\"([^"]+)\\"}`
mainInfoLineCatcherRegex = `self.__pace_f.push\(\[1,\s*"a:(\[.*\])\\n"\]\)`
commonInfoLineCatcherRegex = `self.__pace_f.push\(\[1,\s*\"(\{.*\})\"\]\)`
)

var roomInfoApiForSprintf = "https://live.douyin.com/webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=zh-CN&browser_language=zh-CN&browser_platform=Win32&browser_name=Chrome&browser_version=116.0.0.0&web_rid=%s"
Expand All @@ -33,6 +39,7 @@ func (b *builder) Build(url *url.URL, opt ...live.Option) (live.Live, error) {
return &Live{
BaseLive: internal.NewBaseLive(url, opt...),
responseCookies: make(map[string]string),
isUsingLegacy: false,
}, nil
}

Expand All @@ -42,49 +49,261 @@ func createRandomCookie() string {

type Live struct {
internal.BaseLive
responseCookies map[string]string
responseCookies map[string]string
LastAvailableStringUrlInfos []live.StreamUrlInfo
isUsingLegacy bool
}

func (l *Live) getRoomId() (string, error) {
func (l *Live) getLiveRoomWebPageResponse() (body string, err error) {
cookies := l.Options.Cookies.Cookies(l.Url)
cookieKVs := make(map[string]string)
cookieKVs["__ac_nonce"] = createRandomCookie()
for key, value := range l.responseCookies {
cookieKVs[key] = value
}
for _, item := range cookies {
cookieKVs[item.Name] = item.Value
}

// proxy, _ := url.Parse("http://localhost:8888")
// resp, err := requests.NewSession(&http.Client{
// Transport: &http.Transport{
// Proxy: http.ProxyURL(proxy),
// },
// }).Get(
resp, err := requests.Get(
l.Url.String(),
live.CommonUserAgent,
requests.Cookies(cookieKVs),
requests.Headers(map[string]interface{}{
"Cache-Control": "no-cache",
}),
)
if err != nil {
return "", err
return
}
switch code := resp.StatusCode; code {
case http.StatusOK:
for _, cookie := range resp.Cookies() {
l.responseCookies[cookie.Name] = cookie.Value
}
default:
return "", fmt.Errorf("failed to get page, code: %v, %w", code, live.ErrInternalError)
err = fmt.Errorf("failed to get page, code: %v, %w", code, live.ErrInternalError)
return
}
body, err = resp.Text()
return
}

func (l *Live) getRoomInfoFromBody(body string) (info *live.Info, streamUrlInfos []live.StreamUrlInfo, err error) {
const errorMessageForErrorf = "getRoomInfoFromBody() failed on step %d"
stepNumberForLog := 1
mainInfoLine := utils.Match1(mainInfoLineCatcherRegex, body)
if mainInfoLine == "" {
err = fmt.Errorf(errorMessageForErrorf, stepNumberForLog)
return
}
stepNumberForLog++

// 获取房间信息
mainJson := gjson.Parse(fmt.Sprintf(`"%s"`, mainInfoLine))
if !mainJson.Exists() {
err = fmt.Errorf(errorMessageForErrorf+". Invalid json: %s", stepNumberForLog, mainInfoLine)
return
}

mainJson = gjson.Parse(mainJson.String()).Get("3")
if !mainJson.Exists() {
err = fmt.Errorf(errorMessageForErrorf+". Main json does not exist: %s", stepNumberForLog, mainInfoLine)
return
}

isStreaming := mainJson.Get("state.roomStore.roomInfo.room.status_str").String() == "2"
info = &live.Info{
Live: l,
HostName: mainJson.Get("state.roomStore.roomInfo.anchor.nickname").String(),
RoomName: mainJson.Get("state.roomStore.roomInfo.room.title").String(),
Status: isStreaming,
}
if !isStreaming {
return
}
stepNumberForLog++

// 获取直播流信息
streamIdPath := "state.streamStore.streamData.H264_streamData.common.stream"
streamId := mainJson.Get(streamIdPath).String()
if streamId == "" {
err = fmt.Errorf(errorMessageForErrorf+". %s does not exist: %s", stepNumberForLog, streamIdPath, mainInfoLine)
return
}
sessionIdPath := "state.streamStore.streamData.H264_streamData.common.session_id"
sessionId := mainJson.Get(sessionIdPath).String()
if streamId == "" {
err = fmt.Errorf(errorMessageForErrorf+". %s does not exist: %s", stepNumberForLog, sessionIdPath, mainInfoLine)
return
}
stepNumberForLog++

streamUrlInfos = make([]live.StreamUrlInfo, 0, 4)
reg2, err := regexp.Compile(commonInfoLineCatcherRegex)
if err != nil {
return
}
match2 := reg2.FindAllStringSubmatch(body, -1)
if match2 == nil {
err = fmt.Errorf(errorMessageForErrorf, stepNumberForLog)
return
}
stepNumberForLog++

for _, item := range match2 {
if len(item) < 2 {
err = fmt.Errorf(errorMessageForErrorf+". len(item) = %d", stepNumberForLog, len(item))
return
}
commonJson := gjson.Parse(gjson.Parse(fmt.Sprintf(`"%s"`, item[1])).String())
if !commonJson.Exists() {
err = fmt.Errorf(errorMessageForErrorf+". Not valid json: %s", stepNumberForLog, item[1])
return
}
commonStreamId := commonJson.Get("common.stream").String()
if commonStreamId == "" {
err = fmt.Errorf(errorMessageForErrorf+". No valid common stream ID: %s", stepNumberForLog, item[1])
return
}
if commonStreamId != streamId {
continue
}

commonJson.Get("data").ForEach(func(key, value gjson.Result) bool {
flv := value.Get("main.flv").String() + "&session_id=" + sessionId
var Url *url.URL
Url, err = url.Parse(flv)
if err != nil {
return true
}
paramsString := value.Get("main.sdk_params").String()
paramsJson := gjson.Parse(paramsString)
var description strings.Builder
paramsJson.ForEach(func(key, value gjson.Result) bool {
description.WriteString(key.String())
description.WriteString(": ")
description.WriteString(value.String())
description.WriteString("\n")
return true
})
Priority := 0
resolution := strings.Split(paramsJson.Get("resolution").String(), "x")
if len(resolution) == 2 {
x, err := strconv.Atoi(resolution[0])
if err != nil {
return true
}
y, err := strconv.Atoi(resolution[1])
if err != nil {
return true
}
Priority = x * y
}
streamUrlInfos = append(streamUrlInfos, live.StreamUrlInfo{
Name: key.String(),
Description: description.String(),
Url: Url,
Priority: Priority,
})
return true
})
}
body, err := resp.Text()
sort.Slice(streamUrlInfos, func(i, j int) bool {
return streamUrlInfos[i].Priority > streamUrlInfos[j].Priority
})
stepNumberForLog++

return
}

func (l *Live) GetInfo() (info *live.Info, err error) {
l.isUsingLegacy = false
body, err := l.getLiveRoomWebPageResponse()
if err != nil {
return "", err
l.LastAvailableStringUrlInfos = nil
return
}

var streamUrlInfos []live.StreamUrlInfo
info, streamUrlInfos, err = l.getRoomInfoFromBody(body)
if err == nil {
l.LastAvailableStringUrlInfos = streamUrlInfos
return
}

// fallback
l.isUsingLegacy = true
return l.legacy_GetInfo(body)
}

func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
if !l.isUsingLegacy {
if l.LastAvailableStringUrlInfos != nil {
us = make([]*url.URL, 0, len(l.LastAvailableStringUrlInfos))
for _, urlInfo := range l.LastAvailableStringUrlInfos {
us = append(us, urlInfo.Url)
}
return
}
return nil, fmt.Errorf("failed douyin GetStreamUrls()")
} else {
return l.legacy_GetStreamUrls()
}
}

func (l *Live) GetPlatformCNName() string {
return cnName
}

// ================ legacy functions ================

func (l *Live) legacy_getRoomId(body string) (string, error) {
roomId := utils.Match1(roomIdCatcherRegex, body)
if roomId == "" {
fmt.Println(body)
return "", fmt.Errorf("failed to get RoomId from page, %w", live.ErrInternalError)
}
for _, cookie := range resp.Cookies() {
l.responseCookies[cookie.Name] = cookie.Value
}
return roomId, nil
}

func (l *Live) getRoomInfo() (*gjson.Result, error) {
roomId, err := l.getRoomId()
func (l *Live) legacy_GetStreamUrls() (us []*url.URL, err error) {
var body string
body, err = l.getLiveRoomWebPageResponse()
if err != nil {
l.LastAvailableStringUrlInfos = nil
return
}
data, err := l.legacy_getRoomInfo(body)
if err != nil {
return nil, err
}
var urls []string
data.Get("data.0.stream_url.flv_pull_url").ForEach(func(key, value gjson.Result) bool {
urls = append(urls, value.String())
return true
})
streamData := gjson.Parse(data.Get("data.0.stream_url.live_core_sdk_data.pull_data.stream_data").String())
if streamData.Exists() {
url := streamData.Get("origin.main.flv")
if url.Exists() {
urls = append([]string{url.String()}, urls...)
}
}
return utils.GenUrls(urls...)
}

func (l *Live) legacy_getRoomInfo(body string) (*gjson.Result, error) {
roomId, err := l.legacy_getRoomId(body)
if err != nil {
return nil, err
}

cookies := l.Options.Cookies.Cookies(l.Url)
cookieKVs := make(map[string]string)
cookieKVs["__ac_nonce"] = createRandomCookie()
Expand Down Expand Up @@ -113,49 +332,24 @@ func (l *Live) getRoomInfo() (*gjson.Result, error) {
return nil, fmt.Errorf("failed to get page, code: %v, %w", code, live.ErrInternalError)
}

body, err := resp.Text()
body, err = resp.Text()
if err != nil {
return nil, err
}
result := gjson.Parse(body)
return &result, nil
}

func (l *Live) GetInfo() (info *live.Info, err error) {
data, err := l.getRoomInfo()
// data, err := l.getData()
func (l *Live) legacy_GetInfo(body string) (info *live.Info, err error) {
data, err := l.legacy_getRoomInfo(body)
if err != nil {
return nil, err
}
info = &live.Info{
Live: l,
HostName: data.Get("data.user.nickname").String(),
RoomName: data.Get("data.data.0.title").String(),
Status: data.Get("data.data.0.status").Int() == 2,
HostName: data.Get("user.nickname").String(),
RoomName: data.Get("data.0.title").String(),
Status: data.Get("data.0.status").Int() == 2,
}
return
}

func (l *Live) GetStreamUrls() (us []*url.URL, err error) {
data, err := l.getRoomInfo()
if err != nil {
return nil, err
}
var urls []string
data.Get("data.data.0.stream_url.flv_pull_url").ForEach(func(key, value gjson.Result) bool {
urls = append(urls, value.String())
return true
})
streamData := gjson.Parse(data.Get("data.data.0.stream_url.live_core_sdk_data.pull_data.stream_data").String())
if streamData.Exists() {
url := streamData.Get("data.origin.main.flv")
if url.Exists() {
urls = append([]string{url.String()}, urls...)
}
}
return utils.GenUrls(urls...)
}

func (l *Live) GetPlatformCNName() string {
return cnName
}
10 changes: 10 additions & 0 deletions src/live/lives.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func WithQuality(quality int) Option {

type ID string

type StreamUrlInfo struct {
Url *url.URL
Name string
Description string
Priority int
}

type Live interface {
SetLiveIdByString(string)
GetLiveId() ID
Expand All @@ -118,6 +125,9 @@ func newWrappedLive(live Live, cache gcache.Cache) Live {
func (w *WrappedLive) GetInfo() (*Info, error) {
i, err := w.Live.GetInfo()
if err != nil {
if info, err2 := w.cache.Get(w); err2 == nil {
info.(*Info).RoomName = err.Error()
}
return nil, err
}
if w.cache != nil {
Expand Down
2 changes: 1 addition & 1 deletion src/webapp/src/component/file-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class FileList extends React.Component<Props, IState> {
};

onRowClick = (record: CurrentFolderFile) => {
let path = record.name;
let path = encodeURIComponent(record.name);
if (this.props.match.params.path) {
path = this.props.match.params.path + "/" + path;
}
Expand Down

0 comments on commit 42cb7e3

Please sign in to comment.