Skip to content

Commit

Permalink
[Playlet backend] recommended videos / play next video (#469)
Browse files Browse the repository at this point in the history
* [Playlet backend] recommended videos / play next video

* changelog
  • Loading branch information
iBicha authored Oct 13, 2024
1 parent 65f2b2b commit a9625e3
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Support for autoplay (playing next video) when Playlet backend is selected

## [0.27.1] - 2024-10-08

### Fixed
Expand Down
174 changes: 152 additions & 22 deletions playlet-lib/src/components/Services/Innertube/InnertubeService.bs
Original file line number Diff line number Diff line change
@@ -1,51 +1,65 @@
import "pkg:/source/services/HttpClient.bs"
import "pkg:/source/utils/ObjectUtils.bs"
import "pkg:/source/utils/TimeUtils.bs"

namespace InnertubeService

function GetVideoMetadata(videoId as string, options = invalid as object) as object
request = HttpClient.Post("https://www.youtube.com/youtubei/v1/player", FormatJson(MakePayload(videoId)))
request.Headers({
"accept": "*/*"
"accept-language": "*"
"content-type": "application/json"
"user-agent": "com.google.ios.youtube/18.06.35 (iPhone; CPU iPhone OS 14_4 like Mac OS X; en_US)"
"x-youtube-client-name": "5"
"x-youtube-client-version": "18.06.35"
})

cancellation = invalid
fetchNext = true

if options <> invalid
if options.DoesExist("cancellation")
cancellation = options.cancellation
end if
if options.DoesExist("fetchNext")
fetchNext = options.fetchNext
end if
end if
request.Cancellation(cancellation)

response = request.Await()
playerRequest = CreatePlayerRequest(videoId)
playerRequest.Cancellation(cancellation)
playerRequest.Send()

if not response.IsSuccess()
return response
nextRequest = invalid
nextResponse = invalid
if fetchNext
nextRequest = CreateNextRequest(videoId)
nextRequest.Cancellation(cancellation)
nextRequest.Send()
end if

parsedResponse = ParseInnertubeVideoResponse(response.Json())
playerResponse = playerRequest.Await()
if not playerResponse.IsSuccess()
return playerResponse
end if

if fetchNext
nextResponse = nextRequest.Await()
end if

parsedResponse = ParseInnertubePlayerResponse(playerResponse.Json())

if fetchNext
parsedResponse = ParseInnertubeNextResponse(nextResponse.Json(), parsedResponse)
end if

if parsedResponse <> invalid
if parsedResponse.Success
response.OverrideJson(parsedResponse.Metadata)
playerResponse.OverrideJson(parsedResponse.Metadata)
else
response.OverrideStatusCode(500)
response.OverrideErrorMessage(parsedResponse.Error)
playerResponse.OverrideStatusCode(500)
playerResponse.OverrideErrorMessage(parsedResponse.Error)
end if
end if

return response
return playerResponse
end function

function MakePayload(videoId as string) as object
function CreatePlayerRequest(videoId as string) as object
deviceInfo = CreateObject("roDeviceInfo")
return {

payload = {
"playbackContext": {
"contentPlaybackContext": {
"vis": 0
Expand Down Expand Up @@ -104,9 +118,82 @@ namespace InnertubeService
}
}
}

request = HttpClient.Post("https://www.youtube.com/youtubei/v1/player", FormatJson(payload))
request.Headers({
"accept": "*/*"
"accept-language": "*"
"content-type": "application/json"
"user-agent": "com.google.ios.youtube/18.06.35 (iPhone; CPU iPhone OS 14_4 like Mac OS X; en_US)"
"x-youtube-client-name": "5"
"x-youtube-client-version": "18.06.35"
})

return request
end function

function ParseInnertubeVideoResponse(payload as object) as object
function CreateNextRequest(videoId as string) as object
deviceInfo = CreateObject("roDeviceInfo")

payload = {
"videoId": videoId
"context": {
"client": {
"hl": "en"
"gl": "US"
"remoteHost": ""
"screenDensityFloat": 1
"screenHeightPoints": 1440
"screenPixelDensity": 1
"screenWidthPoints": 2560
"visitorData": ""
"clientName": "WEB"
"clientVersion": "2.20241010.09.00"
"osName": "Windows"
"osVersion": "10.0"
"platform": "DESKTOP"
"clientFormFactor": "UNKNOWN_FORM_FACTOR"
"userInterfaceTheme": "USER_INTERFACE_THEME_LIGHT"
"timeZone": deviceInfo.GetTimeZone()
"originalUrl": "https://www.youtube.com"
"deviceMake": ""
"deviceModel": ""
"browserName": "Chrome"
"browserVersion": "109.0.0.0"
"utcOffsetMinutes": -240
"memoryTotalKbytes": "8000000"
"mainAppWebInfo": {
"graftUrl": "https://www.youtube.com"
"pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN"
"webDisplayMode": "WEB_DISPLAY_MODE_BROWSER"
"isWebNativeShareAvailable": true
}
}
"user": {
"enableSafetyMode": false
"lockedSafetyMode": false
}
"request": {
"useSsl": true
"internalExperimentFlags": []
}
}
}

request = HttpClient.Post("https://www.youtube.com/youtubei/v1/next?prettyPrint=false&alt=json", FormatJson(payload))
request.Headers({
"accept": "*/*"
"accept-language": "*"
"content-type": "application/json"
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0"
"x-youtube-client-name": "1"
"x-youtube-client-version": "2.20241010.09.00"
})

return request
end function

function ParseInnertubePlayerResponse(payload as object) as object
if not IsAssociativeArray(payload)
return {
Success: false
Expand Down Expand Up @@ -169,4 +256,47 @@ namespace InnertubeService
}
end function

function ParseInnertubeNextResponse(payload as object, parsedResponse as object) as object
if not parsedResponse.Success
LogWarn("Invalid parsed response", parsedResponse)
return invalid
end if

if not IsArray(parsedResponse.Metadata.recommendedVideos)
LogWarn("Invalid recommended videos", parsedResponse.Metadata.recommendedVideos)
return parsedResponse
end if

if not IsAssociativeArray(payload)
LogWarn("Invalid payload", payload)
return parsedResponse
end if

endScreenVideos = ObjectUtils.Dig(payload, ["playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results"])
if not IsArray(endScreenVideos)
LogWarn("Invalid end screen videos", endScreenVideos)
return parsedResponse
end if

for each video in endScreenVideos
videoId = ObjectUtils.Dig(video, ["endScreenVideoRenderer", "videoId"])
if videoId = invalid
LogWarn("Invalid video ID in end screen video", video)
continue for
end if

title = ValidString(ObjectUtils.Dig(video, ["endScreenVideoRenderer", "title", "simpleText"]))
author = ValidString(ObjectUtils.Dig(video, ["endScreenVideoRenderer", "shortBylineText", "runs", 0, "text"]))

parsedResponse.Metadata.recommendedVideos.Push({
"type": "video"
"videoId": videoId
"title": title
"author": author
})
end for

return parsedResponse
end function

end namespace
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function VideoQueueViewContentTask(input as object) as object
cacheSeconds: 8640000 ' 100 days
cancellation: cancellation
tryCount: 1
fetchNext: false
}

' TODO:P1: It turns out this might be too much. With a large number of videos
Expand Down
21 changes: 21 additions & 0 deletions playlet-lib/src/source/utils/ObjectUtils.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ObjectUtils

function Dig(obj as object, keys as object) as object
for each key in keys
if IsArray(obj) and IsInt(key) and key >= 0 and key < obj.Count()
obj = obj[key]
continue for
end if

if IsAssociativeArray(obj) and obj.DoesExist(key)
obj = obj[key]
continue for
end if

return invalid
end for

return obj
end function

end namespace

0 comments on commit a9625e3

Please sign in to comment.