Skip to content

Commit d00b31d

Browse files
committed
olivetv: update youtube api
1 parent 5a05d67 commit d00b31d

1 file changed

Lines changed: 131 additions & 34 deletions

File tree

foundation/olivetv/youtube.go

Lines changed: 131 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package olivetv
22

33
import (
4+
"bytes"
45
"fmt"
5-
"strings"
66
"time"
77

8-
"github.com/go-olive/olive/foundation/olivetv/util"
8+
"github.com/imroc/req/v3"
9+
jsoniter "github.com/json-iterator/go"
10+
"golang.org/x/net/html"
911
)
1012

1113
func init() {
@@ -25,46 +27,141 @@ func (this *youtube) Snap(tv *TV) error {
2527
Timestamp: time.Now().Unix(),
2628
}
2729

28-
streamID, err := this.setRoomOn(tv)
29-
if err != nil {
30-
return err
31-
}
32-
return this.setStreamURL(tv, streamID)
30+
return this.set(tv)
3331
}
3432

35-
func (this *youtube) setRoomOn(tv *TV) (string, error) {
36-
channelURL := fmt.Sprintf("https://www.youtube.com/channel/%s", tv.RoomID)
37-
content, err := util.GetURLContent(channelURL)
33+
func (this *youtube) set(tv *TV) error {
34+
liveURL := fmt.Sprintf("https://www.youtube.com/%s/live", tv.RoomID)
35+
resp, err := req.C().R().Get(liveURL)
3836
if err != nil {
39-
return "", err
37+
return fmt.Errorf("get video url failed: %w", err)
4038
}
41-
tv.roomOn = strings.Contains(content, `icon":{"iconType":"LIVE"}}`)
42-
if !tv.roomOn {
43-
return "", nil
39+
videoHTML := resp.Bytes()
40+
41+
// PlayerResponse
42+
prb := this.playerResponseBytes(videoHTML)
43+
if len(prb) == 0 {
44+
return fmt.Errorf("unable to retrieve player response object from watch page")
4445
}
45-
streamID, err := util.Match(`"videoRenderer":{"videoId":"([^"]+)",`, content)
46+
var pr YoutubePlayerResponse
47+
48+
err = jsoniter.Unmarshal(prb, &pr)
4649
if err != nil {
47-
return "", err
50+
return fmt.Errorf("unmarshal pr failed: %w", err)
4851
}
49-
return streamID, nil
52+
53+
tv.streamURL = fmt.Sprintf("https://www.youtube.com/watch?v=%s", pr.VideoDetails.VideoID)
54+
tv.roomOn = pr.VideoDetails.IsLive
55+
tv.roomName = pr.VideoDetails.Title
56+
tv.streamerName = pr.VideoDetails.Author
57+
58+
return nil
5059
}
5160

52-
func (this *youtube) setStreamURL(tv *TV, streamID string) error {
53-
if !tv.roomOn {
54-
return nil
55-
}
56-
// youtube possibly have multiple lives in one channel,
57-
// curruently the program returns the first one.
58-
roomURL := fmt.Sprintf("https://www.youtube.com/watch?v=%s", streamID)
59-
tv.streamURL = roomURL
60-
roomContent, err := util.GetURLContent(roomURL)
61-
if err != nil {
62-
return err
63-
}
64-
title, err := util.Match(`name="title" content="([^"]+)"`, roomContent)
65-
if err != nil {
66-
return err
61+
// playerResponseBytes searches the given HTML for the player response object
62+
func (this *youtube) playerResponseBytes(data []byte) []byte {
63+
playerRespDecl := []byte("var ytInitialPlayerResponse =")
64+
var objData []byte
65+
reader := bytes.NewReader(data)
66+
tokenizer := html.NewTokenizer(reader)
67+
isScript := false
68+
69+
for {
70+
tt := tokenizer.Next()
71+
switch tt {
72+
case html.ErrorToken:
73+
return objData
74+
case html.TextToken:
75+
if isScript {
76+
data := tokenizer.Text()
77+
declStart := bytes.Index(data, playerRespDecl)
78+
if declStart < 0 {
79+
continue
80+
}
81+
82+
// Maybe add a LogTrace in the future for stuff like this
83+
//LogDebug("Found script element with player response in watch page.")
84+
objStart := bytes.Index(data[declStart:], []byte("{")) + declStart
85+
objEnd := bytes.LastIndex(data[objStart:], []byte("};")) + 1 + objStart
86+
87+
if objEnd > objStart {
88+
objData = data[objStart:objEnd]
89+
}
90+
91+
return objData
92+
}
93+
case html.StartTagToken:
94+
tn, _ := tokenizer.TagName()
95+
if string(tn) == "script" {
96+
isScript = true
97+
} else {
98+
isScript = false
99+
}
100+
}
67101
}
68-
tv.roomName = title
69-
return nil
102+
}
103+
104+
type YoutubePlayerResponse struct {
105+
ResponseContext struct {
106+
MainAppWebResponseContext struct {
107+
LoggedOut bool `json:"loggedOut"`
108+
} `json:"mainAppWebResponseContext"`
109+
} `json:"responseContext"`
110+
PlayabilityStatus struct {
111+
Status string `json:"status"`
112+
Reason string `json:"reason"`
113+
LiveStreamability struct {
114+
LiveStreamabilityRenderer struct {
115+
VideoID string `json:"videoId"`
116+
OfflineSlate struct {
117+
LiveStreamOfflineSlateRenderer struct {
118+
ScheduledStartTime string `json:"scheduledStartTime"`
119+
} `json:"liveStreamOfflineSlateRenderer"`
120+
} `json:"offlineSlate"`
121+
PollDelayMs string `json:"pollDelayMs"`
122+
} `json:"liveStreamabilityRenderer"`
123+
} `json:"liveStreamability"`
124+
} `json:"playabilityStatus"`
125+
StreamingData struct {
126+
ExpiresInSeconds string `json:"expiresInSeconds"`
127+
AdaptiveFormats []struct {
128+
Itag int `json:"itag"`
129+
URL string `json:"url"`
130+
MimeType string `json:"mimeType"`
131+
QualityLabel string `json:"qualityLabel,omitempty"`
132+
TargetDurationSec float64 `json:"targetDurationSec"`
133+
} `json:"adaptiveFormats"`
134+
DashManifestURL string `json:"dashManifestUrl"`
135+
HlsManifestURL string `json:"hlsManifestUrl"`
136+
} `json:"streamingData"`
137+
VideoDetails struct {
138+
VideoID string `json:"videoId"`
139+
Title string `json:"title"`
140+
LengthSeconds string `json:"lengthSeconds"`
141+
IsLive bool `json:"isLive"`
142+
ChannelID string `json:"channelId"`
143+
IsOwnerViewing bool `json:"isOwnerViewing"`
144+
ShortDescription string `json:"shortDescription"`
145+
AverageRating float64 `json:"averageRating"`
146+
AllowRatings bool `json:"allowRatings"`
147+
ViewCount string `json:"viewCount"`
148+
Author string `json:"author"`
149+
IsLiveContent bool `json:"isLiveContent"`
150+
} `json:"videoDetails"`
151+
Microformat struct {
152+
PlayerMicroformatRenderer struct {
153+
Thumbnail struct {
154+
Thumbnails []struct {
155+
URL string `json:"url"`
156+
} `json:"thumbnails"`
157+
} `json:"thumbnail"`
158+
LiveBroadcastDetails struct {
159+
IsLiveNow bool `json:"isLiveNow"`
160+
StartTimestamp string `json:"startTimestamp"`
161+
EndTimestamp string `json:"endTimestamp"`
162+
} `json:"liveBroadcastDetails"`
163+
PublishDate string `json:"publishDate"`
164+
UploadDate string `json:"uploadDate"`
165+
} `json:"playerMicroformatRenderer"`
166+
} `json:"microformat"`
70167
}

0 commit comments

Comments
 (0)