From 042d48ca7dd0b9f3ba77d57d04b5bbd861737fcb Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 4 Oct 2024 15:22:53 -0400 Subject: [PATCH 1/6] Playlet innertube backend --- .../Services/Innertube/InnertubeService.bs | 171 ++++++++++++++++++ .../VideoPlayer/VideoContentTask.bs | 8 +- playlet-lib/src/config/preferences.json5 | 17 ++ playlet-lib/src/source/services/HttpClient.bs | 8 + 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 playlet-lib/src/components/Services/Innertube/InnertubeService.bs diff --git a/playlet-lib/src/components/Services/Innertube/InnertubeService.bs b/playlet-lib/src/components/Services/Innertube/InnertubeService.bs new file mode 100644 index 00000000..a204e0e7 --- /dev/null +++ b/playlet-lib/src/components/Services/Innertube/InnertubeService.bs @@ -0,0 +1,171 @@ +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 + 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": "America/Toronto" + "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 diff --git a/playlet-lib/src/components/VideoPlayer/VideoContentTask.bs b/playlet-lib/src/components/VideoPlayer/VideoContentTask.bs index 077d781b..ade0f321 100644 --- a/playlet-lib/src/components/VideoPlayer/VideoContentTask.bs +++ b/playlet-lib/src/components/VideoPlayer/VideoContentTask.bs @@ -1,3 +1,4 @@ +import "pkg:/components/Services/Innertube/InnertubeService.bs" import "pkg:/components/Services/Invidious/InvidiousService.bs" @asynctask @@ -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 diff --git a/playlet-lib/src/config/preferences.json5 b/playlet-lib/src/config/preferences.json5 index 5d855ea3..11d4017c 100644 --- a/playlet-lib/src/config/preferences.json5 +++ b/playlet-lib/src/config/preferences.json5 @@ -32,6 +32,23 @@ }, ], }, + { + displayText: "Backend", + key: "playback.backend", + description: "The backend used for fetching video data", + type: "radio", + defaultValue: "playlet", + options: [ + { + displayText: "Invidious", + value: "invidious", + }, + { + displayText: "Playlet", + value: "playlet", + }, + ], + }, ], }, { diff --git a/playlet-lib/src/source/services/HttpClient.bs b/playlet-lib/src/source/services/HttpClient.bs index 64f5975d..10816626 100644 --- a/playlet-lib/src/source/services/HttpClient.bs +++ b/playlet-lib/src/source/services/HttpClient.bs @@ -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 From ec073e246c7d04eabd41a0fb450c2601ea06e13f Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 4 Oct 2024 15:37:22 -0400 Subject: [PATCH 2/6] Video queue view --- .../components/VideoQueue/VideoQueueView.bs | 1 + .../components/VideoQueue/VideoQueueView.xml | 1 + .../VideoQueue/VideoQueueViewContentTask.bs | 18 ++++++++++++++---- .../VideoQueue/VideoQueueViewUtils.bs | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/playlet-lib/src/components/VideoQueue/VideoQueueView.bs b/playlet-lib/src/components/VideoQueue/VideoQueueView.bs index c498be33..41c02488 100644 --- a/playlet-lib/src/components/VideoQueue/VideoQueueView.bs +++ b/playlet-lib/src/components/VideoQueue/VideoQueueView.bs @@ -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 diff --git a/playlet-lib/src/components/VideoQueue/VideoQueueView.xml b/playlet-lib/src/components/VideoQueue/VideoQueueView.xml index ac4cc6c9..70b9d044 100644 --- a/playlet-lib/src/components/VideoQueue/VideoQueueView.xml +++ b/playlet-lib/src/components/VideoQueue/VideoQueueView.xml @@ -5,6 +5,7 @@ + diff --git a/playlet-lib/src/components/VideoQueue/VideoQueueViewContentTask.bs b/playlet-lib/src/components/VideoQueue/VideoQueueViewContentTask.bs index 86b3b372..dd867fa9 100644 --- a/playlet-lib/src/components/VideoQueue/VideoQueueViewContentTask.bs +++ b/playlet-lib/src/components/VideoQueue/VideoQueueViewContentTask.bs @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/playlet-lib/src/components/VideoQueue/VideoQueueViewUtils.bs b/playlet-lib/src/components/VideoQueue/VideoQueueViewUtils.bs index 0162ba37..55e52695 100644 --- a/playlet-lib/src/components/VideoQueue/VideoQueueViewUtils.bs +++ b/playlet-lib/src/components/VideoQueue/VideoQueueViewUtils.bs @@ -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 From 0f4236984e65d4bbd68a2a719efb76860d914de5 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 4 Oct 2024 15:42:42 -0400 Subject: [PATCH 3/6] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9397e296..33d49345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From dc00c1e14b4f449bc41fcca8160a2edf8048eae8 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 4 Oct 2024 15:53:32 -0400 Subject: [PATCH 4/6] Tweaks --- playlet-lib/src/components/MainScene.bs | 21 +++++-------------- .../Services/Innertube/InnertubeService.bs | 3 ++- playlet-lib/src/config/preferences.json5 | 4 ++-- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/playlet-lib/src/components/MainScene.bs b/playlet-lib/src/components/MainScene.bs index 38259cf4..62ebfddd 100644 --- a/playlet-lib/src/components/MainScene.bs +++ b/playlet-lib/src/components/MainScene.bs @@ -43,22 +43,13 @@ 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." + "Most public Invidious instances don't currently work." + "A new backend to play video data 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)] @@ -66,8 +57,6 @@ function ShowAnnouncement() 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() diff --git a/playlet-lib/src/components/Services/Innertube/InnertubeService.bs b/playlet-lib/src/components/Services/Innertube/InnertubeService.bs index a204e0e7..fa2945cb 100644 --- a/playlet-lib/src/components/Services/Innertube/InnertubeService.bs +++ b/playlet-lib/src/components/Services/Innertube/InnertubeService.bs @@ -44,6 +44,7 @@ namespace InnertubeService end function function MakePayload(videoId as string) as object + deviceInfo = CreateObject("roDeviceInfo") return { "playbackContext": { "contentPlaybackContext": { @@ -80,7 +81,7 @@ namespace InnertubeService "platform": "MOBILE" "clientFormFactor": "UNKNOWN_FORM_FACTOR" "userInterfaceTheme": "USER_INTERFACE_THEME_LIGHT" - "timeZone": "America/Toronto" + "timeZone": deviceInfo.GetTimeZone() "originalUrl": "https://www.youtube.com" "deviceMake": "Apple" "deviceModel": "iPhone10,6" diff --git a/playlet-lib/src/config/preferences.json5 b/playlet-lib/src/config/preferences.json5 index 11d4017c..88b394f0 100644 --- a/playlet-lib/src/config/preferences.json5 +++ b/playlet-lib/src/config/preferences.json5 @@ -35,7 +35,7 @@ { displayText: "Backend", key: "playback.backend", - description: "The backend used for fetching video data", + description: "The backend used for fetching video data. The Playlet backend is experimental", type: "radio", defaultValue: "playlet", options: [ @@ -44,7 +44,7 @@ value: "invidious", }, { - displayText: "Playlet", + displayText: "Playlet (local)", value: "playlet", }, ], From c337c4384579b6191daf9cfeb56808ff577acf3b Mon Sep 17 00:00:00 2001 From: github-action linter Date: Fri, 4 Oct 2024 19:54:31 +0000 Subject: [PATCH 5/6] Lint fix --- .../src/components/MainScene.transpiled.brs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/playlet-lib/src/components/MainScene.transpiled.brs b/playlet-lib/src/components/MainScene.transpiled.brs index 000a86db..b457fbbe 100644 --- a/playlet-lib/src/components/MainScene.transpiled.brs +++ b/playlet-lib/src/components/MainScene.transpiled.brs @@ -42,19 +42,12 @@ 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." + "Most public Invidious instances don't currently work." + "A new backend to play video data 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") @@ -62,8 +55,6 @@ function ShowAnnouncement() 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() From 460d93738c3c87115a92734831b24998266050d4 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 4 Oct 2024 15:56:06 -0400 Subject: [PATCH 6/6] no message --- playlet-lib/src/components/MainScene.bs | 4 ++-- playlet-lib/src/components/MainScene.transpiled.brs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playlet-lib/src/components/MainScene.bs b/playlet-lib/src/components/MainScene.bs index 62ebfddd..a235133d 100644 --- a/playlet-lib/src/components/MainScene.bs +++ b/playlet-lib/src/components/MainScene.bs @@ -46,8 +46,8 @@ function ShowAnnouncement() title = "Announcement #3 - Playlet innertube backend" message = [ - "Most public Invidious instances don't currently work." - "A new backend to play video data has been added to the settings, and it is enabled by default." + "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." ] diff --git a/playlet-lib/src/components/MainScene.transpiled.brs b/playlet-lib/src/components/MainScene.transpiled.brs index b457fbbe..54a336ff 100644 --- a/playlet-lib/src/components/MainScene.transpiled.brs +++ b/playlet-lib/src/components/MainScene.transpiled.brs @@ -44,8 +44,8 @@ end function function ShowAnnouncement() title = "Announcement #3 - Playlet innertube backend" message = [ - "Most public Invidious instances don't currently work." - "A new backend to play video data has been added to the settings, and it is enabled by default." + "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." ]