Merge pull request #150 from 404-Program-not-found/main

Added video specific recommendations, Fixed bugs relating to thumbnails, revamped API
This commit is contained in:
Kenny 2022-03-21 17:44:15 -04:00 committed by GitHub
commit e49021ea29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 343 additions and 203 deletions

View File

@ -1,29 +1,27 @@
// Buttons and methods for testing and demonstration purposes only. Uncomment them to see how it works. Remove to actually implement a implementation
<template>
<div>
<!-- Video Loading Animation -->
<center v-if="recommends.length == 0">
<v-skeleton-loader type="card-avatar, article, actions" />
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
<v-list-item v-for="(video, index) in recommends[0]" :key="index">
<v-list-item v-for="(video, index) in recommends" :key="index">
<v-card class="entry" :to="`/watch?v=${video.id}`">
<v-card-text>
<div style="position: relative">
<v-img :src="video.thumbnail" />
<div v-text="video.metadata.overlay[0]" class="videoRuntimeFloat" style="color: #fff" />
<div
v-text="video.metadata.overlay[0]"
class="videoRuntimeFloat"
style="color: #fff"
/>
</div>
<div v-text="video.title" style="margin-top: 0.5em" />
<div v-text="parseBottom(video)" />
</v-card-text>
</v-card>
</v-list-item>
</div>
</template>
@ -44,22 +42,8 @@
<script>
export default {
data() {
return {
recommends: [],
};
},
// The following code is only a demo for debugging purposes, note that each "shelfRenderer" has a "title" value that seems to align to the categories at the top of the vanilla yt app
mounted() {
this.$youtube
.recommend()
.then((result) => {
console.log(result);
if (result) this.recommends = result;
})
.catch((error) => this.$logger("Home Page", error, true));
props: {
recommends: Array,
},
methods: {

View File

@ -4,5 +4,26 @@
* This is to allow use of "recommended" videos on other pages such as /watch
* -Front
* -->
<recommended />
</template>
<recommended :recommends="recommends" />
</template>
<script>
export default {
data() {
return {
recommends: [],
};
},
// The following code is only a demo for debugging purposes, note that each "shelfRenderer" has a "title" value that seems to align to the categories at the top of the vanilla yt app
mounted() {
this.$youtube
.recommend()
.then((result) => {
if (result) this.recommends = result[0];
})
.catch((error) => this.$logger("Home Page", error, true));
},
};
</script>

View File

@ -1,12 +1,8 @@
<template>
<center class="container">
<v-img src="/icon.svg" width="10em" style="margin-bottom: 1em;" />
<v-progress-circular
size="50"
indeterminate
color="primary"
/>
</center>
<center class="container">
<v-img src="/icon.svg" width="10em" style="margin-bottom: 1em" />
<v-progress-circular size="50" indeterminate color="primary" />
</center>
</template>
<style scoped>
@ -22,45 +18,48 @@
</style>
<script>
import { Plugins } from '@capacitor/core';
const { SplashScreen } = Plugins;
import { SplashScreen } from "@capacitor/splash-screen";
export default {
layout: "empty",
async mounted() {
layout: "empty",
async mounted() {
//--- Hide Splash Screen ---//
await SplashScreen.hide();
//-------------------------------//
//--- Hide Splash Screen ---//
SplashScreen.hide();
//-------------------------------//
//--- Theme Loader Moved From '~/layouts/default.vue' (because this only needs to be run once) -Front ---//
setTimeout(() => {
//Set timeout is required to make it load properly... dont ask me why -Front
const darkTheme = localStorage.getItem("darkTheme");
if (darkTheme == "true") {
this.$vuetify.theme.dark = darkTheme;
//this.$vuetube.statusBar.setDark(); //Not needed unless setLight() is used below -Front
this.$vuetube.statusBar.setBackground(
this.$vuetify.theme.themes.dark.accent
);
//--- Theme Loader Moved From '~/layouts/default.vue' (because this only needs to be run once) -Front ---//
setTimeout(() => { //Set timeout is required to make it load properly... dont ask me why -Front
const darkTheme = localStorage.getItem('darkTheme');
if (darkTheme == "true") {
this.$vuetify.theme.dark = darkTheme;
//this.$vuetube.statusBar.setDark(); //Not needed unless setLight() is used below -Front
this.$vuetube.statusBar.setBackground(this.$vuetify.theme.themes.dark.accent)
const isOled = localStorage.getItem("isOled");
const isOled = localStorage.getItem('isOled')
if(isOled == "true") {
this.$vuetify.theme.themes.dark.accent = '#000',
this.$vuetify.theme.themes.dark.accent2 = '#000',
this.$vuetify.theme.themes.dark.background = '#000'
} else {
this.$vuetify.theme.themes.dark.accent = '#222',
this.$vuetify.theme.themes.dark.accent2 = '#222',
this.$vuetify.theme.themes.dark.background = '#333'
}
if (isOled == "true") {
(this.$vuetify.theme.themes.dark.accent = "#000"),
(this.$vuetify.theme.themes.dark.accent2 = "#000"),
(this.$vuetify.theme.themes.dark.background = "#000");
} else {
//this.$vuetube.statusBar.setLight() //Looks weird -Front
this.$vuetube.statusBar.setBackground(this.$vuetify.theme.themes.light.accent);
(this.$vuetify.theme.themes.dark.accent = "#222"),
(this.$vuetify.theme.themes.dark.accent2 = "#222"),
(this.$vuetify.theme.themes.dark.background = "#333");
}
}, 0);
//-----------------------------------------------------------------------------------------------------------//
} else {
//this.$vuetube.statusBar.setLight() //Looks weird -Front
this.$vuetube.statusBar.setBackground(
this.$vuetify.theme.themes.light.accent
);
}
}, 0);
//-----------------------------------------------------------------------------------------------------------//
await this.$youtube.getAPI()
this.$router.push(`/${localStorage.getItem("startPage") || "home"}`)
}
}
await this.$youtube.getAPI();
this.$router.push(`/${localStorage.getItem("startPage") || "home"}`);
},
};
</script>

View File

@ -2,66 +2,73 @@
<div>
<video controls autoplay :src="vidSrc" width="100%" height="300vh" />
<v-card class="ml-2 mr-2 flat light" flat>
<v-card-title style="padding-top: 0; padding-bottom: 0; font-size: 0.95em;" v-text="title" />
<v-card-title
style="padding-top: 0; padding-bottom: 0; font-size: 0.95em"
v-text="title"
/>
<v-card-text>
<div style="margin-bottom: 1em;">{{ views }} views {{uploaded}}</div>
<!-- Scrolling Div For Interactions --->
<div style="display: flex; margin-bottom: 1em;">
<v-list-item v-for="(item, index) in interactions" :key="index" style="padding: 0; flex: 0 0 20%;">
<div style="margin-bottom: 1em">{{ views }} views {{ uploaded }}</div>
<v-btn text @click="item.action" class="vertical-button" style="padding: 0; margin: 0;" elevation=0 :disabled="item.disabled">
<!-- Scrolling Div For Interactions --->
<div style="display: flex; margin-bottom: 1em">
<v-list-item
v-for="(item, index) in interactions"
:key="index"
style="padding: 0; flex: 0 0 20%"
>
<v-btn
text
@click="item.action"
class="vertical-button"
style="padding: 0; margin: 0"
elevation="0"
:disabled="item.disabled"
>
<v-icon v-text="item.icon" />
<div v-text="item.value || item.name" />
</v-btn>
</v-list-item>
<v-spacer />
<v-btn text @click="showMore = !showMore">
<v-icon v-if="showMore">mdi-chevron-up</v-icon>
<v-icon v-else>mdi-chevron-down</v-icon>
</v-btn>
</div>
<!-- End Scrolling Div For Interactions --->
<hr>
<hr />
<p>Channel Stuff</p>
<hr>
<hr />
</v-card-text>
<div class="scroll-y ml-2 mr-2" v-if="showMore">
{{ description }}
</div>
<div class="scroll-y ml-2 mr-2" v-if="showMore">
{{ description }}
</div>
<!--<v-bottom-sheet v-model="showMore" color="accent2" style="z-index: 9999999;">
<v-sheet style="padding: 1em;">
<v-btn block @click="showMore = !showMore"><v-icon>mdi-chevron-down</v-icon></v-btn><br>
<v-bottom-sheet
v-model="showMore"
color="accent2"
style="z-index: 9999999"
>
<v-sheet style="padding: 1em">
<v-btn block @click="showMore = !showMore"
><v-icon>mdi-chevron-down</v-icon></v-btn
><br />
<div class="scroll-y">
{{ description }}
</div>
</v-sheet>
</v-bottom-sheet>-->
<v-bottom-sheet v-model="share" color="accent2" style="z-index: 9999999;">
<v-sheet style="padding: 1em;">
<div class="scroll-y">
{{ description }}
</div>
</v-sheet>
</v-bottom-sheet>
<!-- <v-bottom-sheet v-model="share" color="accent2" style="z-index: 9999999">
<v-sheet style="padding: 1em">
<div class="scroll-y">
{{ description }}
</div>
</v-sheet>
</v-bottom-sheet> -->
</v-card>
<recommended />
<recommended :recommends="recommends" />
</div>
</template>
@ -74,23 +81,36 @@
</style>
<script>
import recommended from '../components/recommended.vue';
export default {
components: { recommended },
methods: {
dislike() {
},
dislike() {},
share() {
this.share = !this.share;
}
},
},
data() {
return {
interactions: [
{ name: "Likes", icon: "mdi-thumb-up", action: null, value: this.likes, disabled: true },
{ name: "Dislikes", icon: "mdi-thumb-down", action: this.dislike(), value: this.dislikes, disabled: true },
{ name: "Share", icon: "mdi-share", action: this.share(), disabled: true },
{
name: "Likes",
icon: "mdi-thumb-up",
action: null,
value: this.likes,
disabled: true,
},
{
name: "Dislikes",
icon: "mdi-thumb-down",
action: this.dislike(),
value: this.dislikes,
disabled: true,
},
{
name: "Share",
icon: "mdi-share",
action: this.share(),
disabled: true,
},
],
showMore: false,
share: false,
@ -99,32 +119,37 @@ export default {
vidSrc: null,
description: null,
views: null,
}
recommends: [],
};
},
mounted() {
this.likes = 100;
this.$youtube.getVid(this.$route.query.v).then(result => {
console.log('Video info data', result)
result = result.data;
console.log(result.streamingData.formats)
this.vidSrc = result.streamingData.formats[result.streamingData.formats.length-1].url
this.title = result.videoDetails.title
this.description = result.videoDetails.shortDescription;
this.views = result.videoDetails.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
this.$youtube.getVid(this.$route.query.v).then((result) => {
console.log("Video info data", result);
console.log(result.availableResolutions);
this.vidSrc =
result.availableResolutions[result.availableResolutions.length - 1].url; // Takes the highest available resolution with both video and Audio. Note this will be lower than the actual highest resolution
this.title = result.title;
this.description = result.metadata.description; // While this works, I do recommend using the rendered description instead in the future as there are some things a pure string wouldn't work with
this.views = result.metadata.viewCount.toLocaleString();
this.likes = result.metadata.likes.toLocaleString();
this.uploaded = result.metadata.uploadDate;
this.interactions[0].value = result.metadata.likes;
this.recommends = this.$youtube
.viewRecommends(result.renderedData.recommendations)
.filter((element) => {
return element !== undefined;
});
// .catch((error) => this.$logger("Watch", error, true));
console.log("recommendations:", this.recommends);
});
this.$youtube.getRemainingVideoInfo(this.$route.query.v, (data) => {
this.uploaded = data.uploadDate;
this.interactions[0].value = data.likes.toString();
this.$youtube.getReturnYoutubeDislike(this.$route.query.v, (data) => {
this.dislikes = data.dislikes.toLocaleString();
this.interactions[1].value = data.dislikes.toLocaleString();
});
this.$ryd.getDislikes(this.$route.query.v, (data) => {
console.log('real data')
console.log(data)
this.interactions[1].value = data.dislikes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
});
}
}
},
};
</script>

View File

@ -1,7 +1,8 @@
// To centeralize certain values and URLs as for easier debugging and refactoring
// To centralize certain values and URLs as for easier debugging and refactoring
const url = {
YT_URL: 'https://www.youtube.com',
YT_MOBILE: "https://m.youtube.com",
YT_MUSIC_URL: 'https://music.youtube.com',
YT_BASE_API: 'https://www.youtube.com/youtubei/v1',
YT_SUGGESTIONS: "https://suggestqueries.google.com/complete",
@ -11,6 +12,8 @@ const url = {
const ytApiVal = {
VERSION: "16.25",
CLIENTNAME: "ANDROID",
VERSION_WEB: "2.20220318.00.00",
CLIENT_WEB: 2
}
module.exports = {
@ -20,6 +23,7 @@ module.exports = {
LOGGER_NAMES: {
search: "Search",
autoComplete: "AutoComplete",
watch: "Watch",
recommendations: "Recommendations",
init: "Initialize",
innertube: "Innertube"
@ -27,13 +31,16 @@ module.exports = {
INNERTUBE_HEADER: (info) => {
let headers = {
'accept': '*/*',
'user-agent': info.client.userAgent,
accept: '*/*',
'user-agent': info.userAgent,
'accept-language': `${info.hl}-${info.gl},${info.hl};q=0.9`,
'content-type': 'application/json',
'x-goog-authuser': 0,
'x-youtube-client-name': 2,
'x-youtube-client-version': info.client.clientVersion,
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
'x-goog-visitor-id': info.visitorData || "",
'x-youtube-client-name': ytApiVal.CLIENTNAME,
'x-youtube-client-version': ytApiVal.VERSION,
'x-origin': info.originalUrl,
'origin': info.originalUrl,
};
return headers
},
@ -56,5 +63,5 @@ module.exports = {
"visitorData": info.visitorData,
};
return client
}
}
},
}

View File

@ -3,7 +3,7 @@
import { Http } from '@capacitor-community/http';
import { getBetweenStrings } from './utils';
import constants from '../static/constants';
import constants from './constants';
class Innertube {
@ -27,7 +27,10 @@ class Innertube {
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.context = data.INNERTUBE_CONTEXT;
this.logged_in = data.LOGGED_IN;
this.context.client = constants.INNERTUBE_CLIENT(this.context.client)
this.header = constants.INNERTUBE_HEADER(this.context.client)
}
} catch (err) {
@ -79,20 +82,30 @@ class Innertube {
}
static getThumbnail(id, resolution) {
switch (resolution) {
case "min":
return `https://img.youtube.com/vi/${id}/mqdefault.jpg`
case "mid":
return `https://img.youtube.com/vi/${id}/hqdefault.jpg`
default:
return `https://img.youtube.com/vi/${id}/maxresdefault.jpg`
if (resolution == "max"){
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`
let img = new Image();
img.src = url
img.onload = function(){
if (img.height !== 120) return url
};
}
return `https://img.youtube.com/vi/${id}/mqdefault.jpg`
}
}
async getVidInfoAsync(id) {
async getVidAsync(id) {
let data = { context: this.context, videoId: id }
const response = await Http.get({
url: `https://m.youtube.com/watch?v=${id}&pbj=1`,
params: {},
headers: Object.assign(this.header, {
referer: `https://m.youtube.com/watch?v=${id}`,
'x-youtube-client-name': constants.YT_API_VALUES.CLIENT_WEB,
'x-youtube-client-version': constants.YT_API_VALUES.VERSION_WEB})
}).catch((error) => error);
const response = await Http.post({
const responseMobile = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context)
@ -103,7 +116,7 @@ class Innertube {
return {
success: true,
status_code: response.status,
data: response.data
data: {webOutput: response.data, appOutput: responseMobile.data}
};
}
@ -114,7 +127,54 @@ class Innertube {
return rec;
}
async VidInfoAsync(id) {
let response = await this.getVidAsync(id)
if (response.success && (response.data.webOutput[2].playerResponse?.playabilityStatus?.status == ("ERROR" || undefined)))
throw new Error(`Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`)
const responseWeb = response.data.webOutput
const responseApp = response.data.appOutput
const details = responseWeb[2].playerResponse?.videoDetails
const microformat = responseWeb[2].playerResponse?.microformat?.playerMicroformatRenderer
const renderedPanels = responseWeb[3].response?.engagementPanels
const columnUI = responseWeb[3].response?.contents.singleColumnWatchNextResults?.results?.results
const resolutions = responseApp.streamingData
console.log((columnUI.contents).length)
return {
id: details.videoId,
title: details.title || microformat.title?.runs[0].text,
isLive: details.isLiveContent || microformat.liveBroadcastDetails?.isLiveNow || false,
channelName: details.author || microformat.ownerChannelName,
channelUrl: microformat.ownerProfileUrl,
availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: {
description: microformat.description?.runs[0].text,
descriptionShort: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails || microformat.thumbnails?.thumbnails,
isFamilySafe: microformat.isFamilySafe,
availableCountries: microformat.availableCountries,
liveBroadcastDetails: microformat.liveBroadcastDetails,
uploadDate: microformat.uploadDate,
publishDate: microformat.publishDate,
isPrivate: details.isPrivate,
viewCount: details.viewCount || microformat.viewCount,
lengthSeconds: details.lengthSeconds || microformat.lengthSeconds,
likes: parseInt(columnUI?.contents[1]
.slimVideoMetadataSectionRenderer?.contents[1].slimVideoActionBarRenderer?.buttons[0]
.slimMetadataToggleButtonRenderer?.button?.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(/\D/g, '')) // Yes. I know.
},
renderedData: {
description: renderedPanels[0].engagementPanelSectionListRenderer?.content.structuredDescriptionContentRenderer?.items[1].expandableVideoDescriptionBodyRenderer?.descriptionBodyText.runs,
recommendations: columnUI?.contents[(columnUI.contents).length -1].itemSectionRenderer?.contents
}
}
}
}
export default Innertube;
export default Innertube

63
NUXT/plugins/renderers.js Normal file
View File

@ -0,0 +1,63 @@
import Innertube from "./innertube";
import constants from "./constants";
// Pointer object, give a key and it will return with a method
function useRender (video, renderer) {
switch(renderer) {
case "videoWithContextRenderer":
return videoWithContextRenderer(video)
case "gridVideoRenderer":
return gridVideoRenderer(video)
case "compactAutoplayRenderer":
return compactAutoplayRenderer(video)
default:
return undefined
}
}
function gridVideoRenderer(video) {
return {
id: video.videoId,
title: video.title?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0].text : video.longBylineText?.runs[0].text,
channelId: (video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.browseId,
channelURL: `${constants.YT_URL}/${(video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`,
channelThumbnail: video.channelThumbnail?.thumbnails[0],
metadata: {
published: video.publishedTimeText?.runs[0].text,
views: video.shortViewCountText?.runs[0].text,
length: video.lengthText?.runs[0].text,
overlayStyle: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.style),
overlay: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text),
},
};
}
function videoWithContextRenderer(video) {
return {
id: video.videoId,
title: video.headline?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0].text,
channelURL: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl,
channelId: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.browseId,
channelThumbnail: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.thumbnail.thumbnails[0].url,
metadata: {
views: video.shortViewCountText?.runs[0].text,
length: video.lengthText?.runs[0].text,
overlayStyle: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.style),
overlay: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text),
isWatched: video.isWatched,
},
}
}
function compactAutoplayRenderer(video) {
video = video.contents
let item;
if (video) item = video[0]
if (item) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0])
else return undefined
}
export default useRender

View File

@ -1,6 +1,6 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import constants from '../static/constants';
import constants from './constants';
function generateUserID(length = 36) {
const charset =

View File

@ -1,7 +1,7 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import { StatusBar, Style } from '@capacitor/status-bar';
import constants from '../static/constants';
import constants from './constants';
import { hexToRgb, rgbToHex } from './utils';
const module = {

View File

@ -1,7 +1,8 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import Innertube from './innertube'
import constants from '../static/constants';
import constants from './constants';
import useRender from './renderers';
//--- Logger Function ---//
function logger(func, data, isError = false) {
@ -167,13 +168,13 @@ const searchModule = {
}
//--- Recommendations --//
//--- Recommendations ---//
let InnertubeAPI;
// Loads Innertube object. This will be the object used in all future Innertube API calls. Code provided by Lightfire228 (https://github.com/Lightfire228)
// Loads Innertube object. This will be the object used in all future Innertube API calls. getAPI Code provided by Lightfire228 (https://github.com/Lightfire228)
// These are just a way for the backend Javascript to communicate with the front end Vue scripts. Essentially a wrapper inside a wrapper
const recommendationModule = {
const innertubeModule = {
async getAPI() {
if (!InnertubeAPI) {
@ -183,69 +184,49 @@ const recommendationModule = {
},
async getVid(id) {
// temporary test
const html = await Http.get({
url: "https://m.youtube.com/watch?v=U-9M-BjFYMc&t=8s&pbj=1",
params: {},
headers: {
accept: '*/*',
'user-agent': 'Mozilla/5.0 (Linux; Android 10; WP7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36',
'content-type': 'application/json',
'accept-language': 'en-US,en;q=0.9',
'x-goog-authuser': 0,
'x-goog-visitor-id': 'CgtsaVdQdGhfbVNOMCiC0taRBg%3D%3D',
'x-youtube-client-name': 2,
'x-youtube-client-version': '2.20220318.00.00',
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
'x-origin': 'https://m.youtube.com',
origin: 'https://m.youtube.com',
referer: 'https://m.youtube.com/watch?v=U-9M-BjFYMc'
}
}).catch((error) => error);
console.log(html.data)
return InnertubeAPI.getVidInfoAsync(id);
try {
return await InnertubeAPI.VidInfoAsync(id)
} catch (error) {
logger(constants.LOGGER_NAMES.watch, error, true)
}
},
// It just works™
// Front page recommendation
async recommend() {
const response = await InnertubeAPI.getRecommendationsAsync();
if (!response.success) throw new Error("An error occurred and innertube failed to respond")
const contents = response.data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
return contents.map((shelves) => {
const final = contents.map((shelves) => {
const video = shelves.shelfRenderer?.content?.horizontalListRenderer?.items
if (video) return video.map((item) => {
item = item.gridVideoRenderer
if (item) return {
id: item.videoId,
title: item.title?.runs[0].text,
thumbnail: this.getThumbnail(item.videoId),
channel: item.shortBylineText?.runs[0] ? item.shortBylineText.runs[0].text : item.longBylineText?.runs[0].text,
channelURL: `${constants.YT_URL}/${(item.shortBylineText?.runs[0] ? item.shortBylineText.runs[0] : item.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`,
channelThumbnail: item.channelThumbnail?.thumbnails[0],
metadata: {
published: item.publishedTimeText?.runs[0].text,
views: item.shortViewCountText?.runs[0].text,
length: item.publishedTimeText?.runs[0].text,
overlayStyle: item.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.style),
overlay: item.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text),
},
};
else return undefined
if (item) {
const renderedItem = useRender(item[Object.keys(item)[0]], Object.keys(item)[0])
console.log(renderedItem)
return renderedItem
} else {return undefined}
})
})
console.log(final)
return final
},
getThumbnail: (id, resolution) => Innertube.getThumbnail(id, resolution)
// This is the recommendations that exist under videos
viewRecommends(recommendList) {
if (recommendList) return recommendList.map((item) => {
if (item) {
return useRender(item[Object.keys(item)[0]], Object.keys(item)[0])
} else {return undefined}
})
},
}
//--- Start ---//
export default ({ app }, inject) => {
inject('youtube', {...searchModule, ...recommendationModule, })
inject('youtube', {...searchModule, ...innertubeModule })
inject("logger", logger)
}
logger(constants.LOGGER_NAMES.init, "Program Started");