Skip to content

Commit

Permalink
Playlet innertube backend (#463)
Browse files Browse the repository at this point in the history
* Playlet innertube backend

* Video queue view

* changelog

* Tweaks

* Lint fix

* no message

---------

Co-authored-by: github-action linter <githubaction@githubaction.com>
  • Loading branch information
iBicha and github-action linter authored Oct 4, 2024
1 parent 8cb9d71 commit d0d45bc
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Experimental Playlet backend to play videos using Innertube
- Partial Spanish translations (Thanks to Kamborio and gallegonovato)

## [0.26.0] - 2024-09-23
Expand Down
21 changes: 5 additions & 16 deletions playlet-lib/src/components/MainScene.bs
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,20 @@ function GetDeviceFriendlyName() as string
end function

function ShowAnnouncement()
title = "Announcement #2 - web app hot fix"
title = "Announcement #3 - Playlet innertube backend"

message = [
"Invidious servers continue being blocked by YouTube. https://github.com/iBicha/playlet/issues/400"
"A temporary workaround has been implemented which relies on your Roku device instead of Invidious for streaming data. This workaround is limited, doesn't always work, and doesn't have captions.",
"How to use:"
]

bulletText = [
`Go to the "Remote" screen, and open Playlet Web App in your browser`
"In the web app, tap the video you want to play"
`Choose "Play on ${GetDeviceFriendlyName()} (HOT FIX)"`
]

bottomMessage = [
"We apologize for the inconvenience."
"Currently, most public Invidious instances don't currently work."
"Because of that, a new backend to play videos has been added to the settings, and it is enabled by default."
"If you would like to use your Invidious instance for playback, you can change the backend in the settings."
"Thank you."
]

buttons = [Tr(Locale.Buttons.OK)]

dialog = CreateObject("roSGNode", "SimpleDialog")
dialog.title = title
dialog.message = message
dialog.bulletText = bulletText
dialog.bottomMessage = bottomMessage
dialog.buttons = buttons
deviceInfo = CreateObject("roDeviceInfo")
displaySize = deviceInfo.GetDisplaySize()
Expand Down
19 changes: 5 additions & 14 deletions playlet-lib/src/components/MainScene.transpiled.brs
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,19 @@ function GetDeviceFriendlyName() as string
end function

function ShowAnnouncement()
title = "Announcement #2 - web app hot fix"
title = "Announcement #3 - Playlet innertube backend"
message = [
"Invidious servers continue being blocked by YouTube. https://github.com/iBicha/playlet/issues/400"
"A temporary workaround has been implemented which relies on your Roku device instead of Invidious for streaming data. This workaround is limited, doesn't always work, and doesn't have captions."
"How to use:"
]
bulletText = [
"Go to the " + chr(34) + "Remote" + chr(34) + " screen, and open Playlet Web App in your browser"
"In the web app, tap the video you want to play"
("Choose " + chr(34) + "Play on " + bslib_toString(GetDeviceFriendlyName()) + " (HOT FIX)" + chr(34))
]
bottomMessage = [
"We apologize for the inconvenience."
"Currently, most public Invidious instances don't currently work."
"Because of that, a new backend to play videos has been added to the settings, and it is enabled by default."
"If you would like to use your Invidious instance for playback, you can change the backend in the settings."
"Thank you."
]
buttons = [
Tr("OK")
]
dialog = CreateObject("roSGNode", "SimpleDialog")
dialog.title = title
dialog.message = message
dialog.bulletText = bulletText
dialog.bottomMessage = bottomMessage
dialog.buttons = buttons
deviceInfo = CreateObject("roDeviceInfo")
displaySize = deviceInfo.GetDisplaySize()
Expand Down
172 changes: 172 additions & 0 deletions playlet-lib/src/components/Services/Innertube/InnertubeService.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import "pkg:/source/services/HttpClient.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

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

response = request.Await()

if not response.IsSuccess()
return response
end if

parsedResponse = ParseInnertubeVideoResponse(response.Json())

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

return response
end function

function MakePayload(videoId as string) as object
deviceInfo = CreateObject("roDeviceInfo")
return {
"playbackContext": {
"contentPlaybackContext": {
"vis": 0
"splay": false
"referer": "https://www.youtube.com/watch?v=" + videoId
"currentUrl": "/watch?v=" + videoId
"autonavState": "STATE_ON"
"autoCaptionsDefaultOn": false
"html5Preference": "HTML5_PREF_WANTS"
"lactMilliseconds": "-1"
}
}
"attestationRequest": {
"omitBotguardData": true
}
"racyCheckOk": true
"contentCheckOk": true
"videoId": videoId
"context": {
"client": {
"hl": "en"
"gl": "US"
"remoteHost": ""
"screenDensityFloat": 1
"screenHeightPoints": 1440
"screenPixelDensity": 1
"screenWidthPoints": 2560
"visitorData": ""
"clientName": "iOS"
"clientVersion": "18.06.35"
"osName": "iOS"
"osVersion": "10.0"
"platform": "MOBILE"
"clientFormFactor": "UNKNOWN_FORM_FACTOR"
"userInterfaceTheme": "USER_INTERFACE_THEME_LIGHT"
"timeZone": deviceInfo.GetTimeZone()
"originalUrl": "https://www.youtube.com"
"deviceMake": "Apple"
"deviceModel": "iPhone10,6"
"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": []
}
}
}
end function

function ParseInnertubeVideoResponse(payload as object) as object
if not IsAssociativeArray(payload)
return {
Success: false
Error: "Invalid payload"
}
end if

playabilityStatus = payload["playabilityStatus"]
if not IsAssociativeArray(playabilityStatus)
return {
Success: false
Error: "Invalid playability status"
}
end if

if playabilityStatus["status"] <> "OK"
return {
Success: false
Error: playabilityStatus["reason"]
}
end if

videoDetails = payload["videoDetails"]
if not IsAssociativeArray(videoDetails)
return {
Success: false
Error: "Invalid video details"
}
end if

streamingData = payload["streamingData"]
if not IsAssociativeArray(streamingData)
return {
Success: false
Error: "Invalid streaming data"
}
end if

videoInfo = {
"type": "video"
"title": videoDetails["title"]
"videoId": videoDetails["videoId"]
"videoThumbnails": videoDetails["thumbnail"]["thumbnails"]
"storyboards": []
"viewCount": videoDetails["viewCount"].ToInt()
"author": videoDetails["author"]
"authorId": videoDetails["channelId"]
"lengthSeconds": videoDetails["lengthSeconds"].ToInt()
"liveNow": videoDetails["isLiveContent"]
"hlsUrl": streamingData["hlsManifestUrl"]
"adaptiveFormats": []
"formatStreams": []
"captions": []
"recommendedVideos": []
}

return {
Success: true
Metadata: videoInfo
}
end function

end namespace
8 changes: 7 additions & 1 deletion playlet-lib/src/components/VideoPlayer/VideoContentTask.bs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "pkg:/components/Services/Innertube/InnertubeService.bs"
import "pkg:/components/Services/Invidious/InvidiousService.bs"

@asynctask
Expand All @@ -17,7 +18,12 @@ function VideoContentTask(input as object) as object

service = new Invidious.InvidiousService(invidiousNode)

response = service.GetVideoMetadata(contentNode.videoId, { cancellation: m.top.cancellation })
backend = preferencesNode["playback.backend"]
if backend = "playlet"
response = InnertubeService.GetVideoMetadata(contentNode.videoId, { cancellation: m.top.cancellation })
else
response = service.GetVideoMetadata(contentNode.videoId, { cancellation: m.top.cancellation })
end if

if m.top.cancel
return invalid
Expand Down
1 change: 1 addition & 0 deletions playlet-lib/src/components/VideoQueue/VideoQueueView.bs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ function LoadVideoDetailsIfNeeded() as void
m.loadDetailsTask = AsyncTask.Start(Tasks.VideoQueueViewContentTask, {
videoNodes: m.videosDetailsToLoad
invidious: m.top.invidious
preferences: m.top.preferences
index: indexInLoadingArray
})
end function
Expand Down
1 change: 1 addition & 0 deletions playlet-lib/src/components/VideoQueue/VideoQueueView.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<field id="videoQueue" type="node" />
<field id="appController" type="node" />
<field id="invidious" type="node" />
<field id="preferences" type="node" />
<function name="Show" />
</interface>

Expand Down
18 changes: 14 additions & 4 deletions playlet-lib/src/components/VideoQueue/VideoQueueViewContentTask.bs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "pkg:/components/Services/Innertube/InnertubeService.bs"
import "pkg:/components/Services/Invidious/InvidiousService.bs"
import "pkg:/components/Services/Invidious/InvidiousToContentNode.bs"
import "pkg:/source/services/HttpClient.bs"
Expand All @@ -7,12 +8,15 @@ import "pkg:/source/utils/MathUtils.bs"
function VideoQueueViewContentTask(input as object) as object
videoNodes = input.videoNodes
invidiousNode = input.invidious
preferencesNode = input.preferences
service = new Invidious.InvidiousService(invidiousNode)
instance = service.GetInstance()
cancellation = m.top.cancellation

index = MathUtils.Clamp(input.index - 2, 0, videoNodes.Count() - 1)

backend = preferencesNode["playback.backend"]

requestOptions = {
cacheSeconds: 8640000 ' 100 days
cancellation: cancellation
Expand All @@ -24,7 +28,7 @@ function VideoQueueViewContentTask(input as object) as object
' Need to change it to load on demand using render tracking
for i = index to videoNodes.Count() - 1
videoNode = videoNodes[i]
if not LoadVideoDetail(videoNode, service, instance, requestOptions)
if not LoadVideoDetail(videoNode, service, instance, requestOptions, backend)
' Sleep for a bit to avoid creating too much traffic
sleep(500)
end if
Expand All @@ -36,7 +40,7 @@ function VideoQueueViewContentTask(input as object) as object

for i = index - 1 to 0 step -1
videoNode = videoNodes[i]
if not LoadVideoDetail(videoNode, service, instance, requestOptions)
if not LoadVideoDetail(videoNode, service, instance, requestOptions, backend)
' Sleep for a bit to avoid creating too much traffic
sleep(1000)
end if
Expand All @@ -50,9 +54,15 @@ function VideoQueueViewContentTask(input as object) as object
end function

' Returns true if the video was loaded from cache.
function LoadVideoDetail(videoNode as object, service as object, instance as string, requestOptions as object) as boolean
function LoadVideoDetail(videoNode as object, service as object, instance as string, requestOptions as object, backend as string) as boolean
videoId = videoNode.videoId
response = service.GetVideoMetadata(videoId, requestOptions)

if backend = "playlet"
response = InnertubeService.GetVideoMetadata(videoId, requestOptions)
else
response = service.GetVideoMetadata(videoId, requestOptions)
end if

metadata = response.Json()

if not response.IsSuccess() or metadata = invalid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace VideoQueueViewUtils
videoQueueView.videoQueue = videoQueue
videoQueueView.appController = appController
videoQueueView.invidious = videoQueue.invidious
videoQueueView.preferences = videoQueue.preferences
appController@.PushScreen(videoQueueView)

content = videoQueue.content
Expand Down
17 changes: 17 additions & 0 deletions playlet-lib/src/config/preferences.json5
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@
},
],
},
{
displayText: "Backend",
key: "playback.backend",
description: "The backend used for fetching video data. The Playlet backend is experimental",
type: "radio",
defaultValue: "playlet",
options: [
{
displayText: "Invidious",
value: "invidious",
},
{
displayText: "Playlet (local)",
value: "playlet",
},
],
},
],
},
{
Expand Down
8 changes: 8 additions & 0 deletions playlet-lib/src/source/services/HttpClient.bs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,14 @@ namespace HttpClient
m._text = text
end function

function OverrideJson(json as object)
m._json = json
end function

function OverrideErrorMessage(errorMessage as string)
m._errorMessage = errorMessage
end function

function IsSuccess() as boolean
statusCode = m.StatusCode()
return statusCode >= 200 and statusCode < 400
Expand Down

0 comments on commit d0d45bc

Please sign in to comment.