mirror of https://github.com/VueTubeApp/VueTube
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:
commit
e49021ea29
|
@ -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: {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 =
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue