Skip to content

Commit

Permalink
Check for membership streams when given a channel link and cookies
Browse files Browse the repository at this point in the history
If a channel link and cookies are provided, membership is checked
first, and then non-membership streams are looked for second.
Closes #58
  • Loading branch information
Kethsar committed Aug 20, 2023
1 parent 3c84a82 commit 1790a76
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 27 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ Attempt to archive a given Youtube livestream from the start. This is most usefu

Download the latest pre-release from [the releases page](https://github.com/Kethsar/ytarchive/releases)

Alternatively, if you have Go properly installed and set up, run `go install github.com/Kethsar/ytarchive@master`

`@master` is required because of some bullshit caching Go package proxies do. Should have used Rust... (should have been a better dev)
Alternatively, if you have Go properly installed and set up, run `go install github.com/Kethsar/ytarchive@dev`

## Usage

Expand Down
117 changes: 93 additions & 24 deletions player_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,19 @@ type YtInitialData struct {
} `json:"endpoint"`
Content struct {
Richgridrenderer struct {
Contents []YtInitialDataContent `json:"contents"`
Contents []RichGridContent `json:"contents"`
} `json:"richGridRenderer"`
SectionListRenderer struct {
Contents []SectionListContent `json:"contents"`
} `json:"sectionListRenderer"`
} `json:"content"`
} `json:"tabRenderer"`
} `json:"tabs"`
} `json:"twoColumnBrowseResultsRenderer"`
} `json:"contents"`
}

type YtInitialDataContent struct {
type RichGridContent struct {
Richitemrenderer struct {
Content struct {
Videorenderer struct {
Expand All @@ -157,6 +160,21 @@ type YtInitialDataContent struct {
} `json:"richItemRenderer"`
}

type SectionListContent struct {
ItemSectionRenderer struct {
Contents []struct {
Videorenderer struct {
Videoid string `json:"videoId"`
Thumbnailoverlays []struct {
Thumbnailoverlaytimestatusrenderer struct {
Style string `json:"style"`
} `json:"thumbnailOverlayTimeStatusRenderer"`
} `json:"thumbnailOverlays"`
} `json:"videoRenderer"`
} `json:"contents"`
} `json:"itemSectionRenderer"`
}

// Search the given HTML for the player response object
func GetJsonFromHtml(htmlData []byte, jsonDecl []byte) []byte {
var objData []byte
Expand Down Expand Up @@ -199,9 +217,9 @@ func GetJsonFromHtml(htmlData []byte, jsonDecl []byte) []byte {
}
}

func GetNewestLiveStream(liveUrl string) string {
func GetNewestStreamFromStreams(liveUrl string) string {
initialData := &YtInitialData{}
var contents []YtInitialDataContent
var contents []RichGridContent
streamsUrl := strings.Replace(liveUrl, "/live", "/streams", 1)
streamsHtml := DownloadData(streamsUrl)
ytInitialData := GetJsonFromHtml(streamsHtml, ytInitialDataDecl)
Expand All @@ -219,23 +237,68 @@ func GetNewestLiveStream(liveUrl string) string {
}

for _, content := range contents {
isLive := false
for _, thumbnailRenderer := range content.Richitemrenderer.Content.Videorenderer.Thumbnailoverlays {
videoRenderer := content.Richitemrenderer.Content.Videorenderer

for _, thumbnailRenderer := range videoRenderer.Thumbnailoverlays {
if thumbnailRenderer.Thumbnailoverlaytimestatusrenderer.Style == "LIVE" {
isLive = true
break
streamUrl = fmt.Sprintf("https://www.youtube.com/watch?v=%s", videoRenderer.Videoid)
return streamUrl
}
}
}

return streamUrl
}

func GetNewestStreamFromMembership(liveUrl string) string {
initialData := &YtInitialData{}
var contents []SectionListContent
streamsUrl := strings.Replace(liveUrl, "/live", "/membership", 1)
streamsHtml := DownloadData(streamsUrl)
ytInitialData := GetJsonFromHtml(streamsHtml, ytInitialDataDecl)
streamUrl := ""

err := json.Unmarshal(ytInitialData, initialData)
if err != nil {
return streamUrl
}

for _, tab := range initialData.Contents.Twocolumnbrowseresultsrenderer.Tabs {
if strings.HasSuffix(tab.Tabrenderer.Endpoint.Commandmetadata.Webcommandmetadata.URL, "/membership") {
contents = tab.Tabrenderer.Content.SectionListRenderer.Contents
}
}

if isLive {
streamUrl = fmt.Sprintf("https://www.youtube.com/watch?v=%s", content.Richitemrenderer.Content.Videorenderer.Videoid)
break
for _, content := range contents {
for _, videoContent := range content.ItemSectionRenderer.Contents {
videoRenderer := videoContent.Videorenderer

for _, thumbnailRenderer := range videoRenderer.Thumbnailoverlays {
if thumbnailRenderer.Thumbnailoverlaytimestatusrenderer.Style == "LIVE" {
streamUrl = fmt.Sprintf("https://www.youtube.com/watch?v=%s", videoRenderer.Videoid)
return streamUrl
}
}
}
}

return streamUrl
}

func (di *DownloadInfo) GetNewestLiveStream() string {
streamUrl := ""

if di.CookiesURL != nil {
streamUrl = GetNewestStreamFromMembership(di.URL)
}

if len(streamUrl) == 0 {
streamUrl = GetNewestStreamFromStreams(di.URL)
}

return streamUrl
}

// At the time of adding, retrieving the player response from the api while
// claiming to be the android client seems to result in unthrottled download
// URLs. Credit to yt-dlp devs for POST data and headers.
Expand Down Expand Up @@ -297,6 +360,24 @@ func (di *DownloadInfo) DownloadAndroidPlayerResponse() (*PlayerResponse, error)
return pr, nil
}

func (di *DownloadInfo) GetVideoHtml() []byte {
var videoHtml []byte

if di.LiveURL {
streamUrl := di.GetNewestLiveStream()

if len(streamUrl) > 0 {
videoHtml = DownloadData(streamUrl)
}
}

if len(videoHtml) == 0 {
videoHtml = DownloadData(di.URL)
}

return videoHtml
}

// Get the player response object from youtube
func (di *DownloadInfo) GetPlayerResponse(videoHtml []byte) (*PlayerResponse, error) {
pr := &PlayerResponse{}
Expand Down Expand Up @@ -343,21 +424,9 @@ func (di *DownloadInfo) GetPlayablePlayerResponse() (retrieved int, pr *PlayerRe
}

for {
videoHtml := DownloadData(di.URL)
videoHtml := di.GetVideoHtml()
pr, err = di.GetPlayerResponse(videoHtml)

if err != nil && isLiveURL {
if !waitOnLiveURL {
LogDebug("Could not get player response data from /live, scraping /streams")
}
streamUrl := GetNewestLiveStream(di.URL)

if len(streamUrl) > 0 {
videoHtml = DownloadData(streamUrl)
pr, err = di.GetPlayerResponse(videoHtml)
}
}

if err != nil {
if waitOnLiveURL {
if len(selectedQualities) < 1 {
Expand Down

0 comments on commit 1790a76

Please sign in to comment.