11package olivetv
22
33import (
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
1113func 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