Skip to content

Commit

Permalink
olivetv: update youtube api
Browse files Browse the repository at this point in the history
  • Loading branch information
luxcgo committed Jun 21, 2023
1 parent 5a05d67 commit d00b31d
Showing 1 changed file with 131 additions and 34 deletions.
165 changes: 131 additions & 34 deletions foundation/olivetv/youtube.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package olivetv

import (
"bytes"
"fmt"
"strings"
"time"

"github.com/go-olive/olive/foundation/olivetv/util"
"github.com/imroc/req/v3"
jsoniter "github.com/json-iterator/go"
"golang.org/x/net/html"
)

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

streamID, err := this.setRoomOn(tv)
if err != nil {
return err
}
return this.setStreamURL(tv, streamID)
return this.set(tv)
}

func (this *youtube) setRoomOn(tv *TV) (string, error) {
channelURL := fmt.Sprintf("https://www.youtube.com/channel/%s", tv.RoomID)
content, err := util.GetURLContent(channelURL)
func (this *youtube) set(tv *TV) error {
liveURL := fmt.Sprintf("https://www.youtube.com/%s/live", tv.RoomID)
resp, err := req.C().R().Get(liveURL)
if err != nil {
return "", err
return fmt.Errorf("get video url failed: %w", err)
}
tv.roomOn = strings.Contains(content, `icon":{"iconType":"LIVE"}}`)
if !tv.roomOn {
return "", nil
videoHTML := resp.Bytes()

// PlayerResponse
prb := this.playerResponseBytes(videoHTML)
if len(prb) == 0 {
return fmt.Errorf("unable to retrieve player response object from watch page")
}
streamID, err := util.Match(`"videoRenderer":{"videoId":"([^"]+)",`, content)
var pr YoutubePlayerResponse

err = jsoniter.Unmarshal(prb, &pr)
if err != nil {
return "", err
return fmt.Errorf("unmarshal pr failed: %w", err)
}
return streamID, nil

tv.streamURL = fmt.Sprintf("https://www.youtube.com/watch?v=%s", pr.VideoDetails.VideoID)
tv.roomOn = pr.VideoDetails.IsLive
tv.roomName = pr.VideoDetails.Title
tv.streamerName = pr.VideoDetails.Author

return nil
}

func (this *youtube) setStreamURL(tv *TV, streamID string) error {
if !tv.roomOn {
return nil
}
// youtube possibly have multiple lives in one channel,
// curruently the program returns the first one.
roomURL := fmt.Sprintf("https://www.youtube.com/watch?v=%s", streamID)
tv.streamURL = roomURL
roomContent, err := util.GetURLContent(roomURL)
if err != nil {
return err
}
title, err := util.Match(`name="title" content="([^"]+)"`, roomContent)
if err != nil {
return err
// playerResponseBytes searches the given HTML for the player response object
func (this *youtube) playerResponseBytes(data []byte) []byte {
playerRespDecl := []byte("var ytInitialPlayerResponse =")
var objData []byte
reader := bytes.NewReader(data)
tokenizer := html.NewTokenizer(reader)
isScript := false

for {
tt := tokenizer.Next()
switch tt {
case html.ErrorToken:
return objData
case html.TextToken:
if isScript {
data := tokenizer.Text()
declStart := bytes.Index(data, playerRespDecl)
if declStart < 0 {
continue
}

// Maybe add a LogTrace in the future for stuff like this
//LogDebug("Found script element with player response in watch page.")
objStart := bytes.Index(data[declStart:], []byte("{")) + declStart
objEnd := bytes.LastIndex(data[objStart:], []byte("};")) + 1 + objStart

if objEnd > objStart {
objData = data[objStart:objEnd]
}

return objData
}
case html.StartTagToken:
tn, _ := tokenizer.TagName()
if string(tn) == "script" {
isScript = true
} else {
isScript = false
}
}
}
tv.roomName = title
return nil
}

type YoutubePlayerResponse struct {
ResponseContext struct {
MainAppWebResponseContext struct {
LoggedOut bool `json:"loggedOut"`
} `json:"mainAppWebResponseContext"`
} `json:"responseContext"`
PlayabilityStatus struct {
Status string `json:"status"`
Reason string `json:"reason"`
LiveStreamability struct {
LiveStreamabilityRenderer struct {
VideoID string `json:"videoId"`
OfflineSlate struct {
LiveStreamOfflineSlateRenderer struct {
ScheduledStartTime string `json:"scheduledStartTime"`
} `json:"liveStreamOfflineSlateRenderer"`
} `json:"offlineSlate"`
PollDelayMs string `json:"pollDelayMs"`
} `json:"liveStreamabilityRenderer"`
} `json:"liveStreamability"`
} `json:"playabilityStatus"`
StreamingData struct {
ExpiresInSeconds string `json:"expiresInSeconds"`
AdaptiveFormats []struct {
Itag int `json:"itag"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
QualityLabel string `json:"qualityLabel,omitempty"`
TargetDurationSec float64 `json:"targetDurationSec"`
} `json:"adaptiveFormats"`
DashManifestURL string `json:"dashManifestUrl"`
HlsManifestURL string `json:"hlsManifestUrl"`
} `json:"streamingData"`
VideoDetails struct {
VideoID string `json:"videoId"`
Title string `json:"title"`
LengthSeconds string `json:"lengthSeconds"`
IsLive bool `json:"isLive"`
ChannelID string `json:"channelId"`
IsOwnerViewing bool `json:"isOwnerViewing"`
ShortDescription string `json:"shortDescription"`
AverageRating float64 `json:"averageRating"`
AllowRatings bool `json:"allowRatings"`
ViewCount string `json:"viewCount"`
Author string `json:"author"`
IsLiveContent bool `json:"isLiveContent"`
} `json:"videoDetails"`
Microformat struct {
PlayerMicroformatRenderer struct {
Thumbnail struct {
Thumbnails []struct {
URL string `json:"url"`
} `json:"thumbnails"`
} `json:"thumbnail"`
LiveBroadcastDetails struct {
IsLiveNow bool `json:"isLiveNow"`
StartTimestamp string `json:"startTimestamp"`
EndTimestamp string `json:"endTimestamp"`
} `json:"liveBroadcastDetails"`
PublishDate string `json:"publishDate"`
UploadDate string `json:"uploadDate"`
} `json:"playerMicroformatRenderer"`
} `json:"microformat"`
}

0 comments on commit d00b31d

Please sign in to comment.