mirror of
https://github.com/VueTubeApp/VueTube
synced 2024-11-26 21:23:02 +00:00
Merge pull request #92 from 404-Program-not-found/main
Backend + Rudimentary front end for home page recommendation Completed
This commit is contained in:
commit
9850b058b1
6 changed files with 166 additions and 55 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ dist
|
||||||
package-lock.json
|
package-lock.json
|
||||||
temp.js
|
temp.js
|
||||||
temp.json
|
temp.json
|
||||||
|
.vscode/settings.json
|
||||||
|
|
|
@ -1,24 +1,68 @@
|
||||||
// Buttons and methods for testing and demonstration purposes only. Uncomment them to see how it works. Remove to actually implement a implementation
|
// Buttons and methods for testing and demonstration purposes only. Uncomment them to see how it works. Remove to actually implement a implementation
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<center>
|
<div>
|
||||||
<v-img contain style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" />
|
|
||||||
<h1 class="grey--text">Page Under Construction</h1>
|
<center style="padding-top: 3em;" v-if="recommends == null">
|
||||||
<p class="grey--text">Please read the VueTube FAQ for more information.</p>
|
<v-progress-circular
|
||||||
<!-- <button @click="debugRecommend">Test Button</button>
|
size="50"
|
||||||
<button @click="debugVideo">Test Button (Video)</button> -->
|
indeterminate
|
||||||
</center>
|
color="primary"
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
<center>
|
||||||
|
<v-list-item v-for="(video, index) in recommends" :key="index">
|
||||||
|
<v-card class="entry" :to="`/watch?v=${video.videoId}`">
|
||||||
|
<v-card-text>
|
||||||
|
<div style="position: relative;">
|
||||||
|
<v-img :src="getThumbnail(video.videoId,'min')" />
|
||||||
|
<p v-text="video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text.runs[0].text" class="videoRuntimeFloat" style="color: #fff;" />
|
||||||
|
</div>
|
||||||
|
<div v-text="video.title.runs[0].text" style="margin-top: 0.5em;" />
|
||||||
|
<div v-text="`${video.shortViewCountText.runs[0].text} • ${video.publishedTimeText ? video.publishedTimeText.runs[0].text : video.shortViewCountText.runs[1].text}`" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-list-item>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
recommends: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
const vm = this;
|
||||||
|
this.$youtube.recommend().then(
|
||||||
|
result => {
|
||||||
|
const videoList = []
|
||||||
|
console.log(result)
|
||||||
|
const recommendContent = result.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents // I feel like I've committed programming sin
|
||||||
|
recommendContent.forEach(function (contents, index) {
|
||||||
|
contents.shelfRenderer.content.horizontalListRenderer.items.forEach(function (item, index) {
|
||||||
|
const video = item.gridVideoRenderer
|
||||||
|
console.log(video)
|
||||||
|
console.log(video.onTap)
|
||||||
|
videoList.push(video)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
vm.recommends = videoList;
|
||||||
|
}
|
||||||
|
).catch ((error) => {
|
||||||
|
this.$logger("Home Page", error, true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
// debugRecommend () {
|
getThumbnail(id, resolution) {
|
||||||
// console.log(this.$youtube.recommend("test", false))
|
return this.$youtube.getThumbnail(id, resolution)
|
||||||
// },
|
}
|
||||||
// debugVideo () {
|
|
||||||
// console.log(this.$youtube.getVid("WhWc3b3KhnY"))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -11,8 +11,9 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
layout: "empty",
|
layout: "empty",
|
||||||
mounted() {
|
async mounted() {
|
||||||
this.$router.push(`/${localStorage.getItem("startPage") || "home"}`);
|
await this.$youtube.getAPI()
|
||||||
|
this.$router.push(`/${localStorage.getItem("startPage") || "home"}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,37 +18,37 @@ class Innertube {
|
||||||
return typeof this.ErrorCallback === "function"
|
return typeof this.ErrorCallback === "function"
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
async initAsync() {
|
||||||
Http.get({ url: constants.URLS.YT_URL, params: { hl: "en" } })
|
const html = await Http.get({ url: constants.URLS.YT_URL, params: { hl: "en" } }).catch((error) => error);
|
||||||
.then(result => {
|
try {
|
||||||
if (result instanceof Error && this.checkErrorCallback) this.ErrorCallback(result.message, true);
|
if (html instanceof Error && this.checkErrorCallback) this.ErrorCallback(html.message, true);
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(getBetweenStrings(result.data, 'ytcfg.set(', ');'));
|
const data = JSON.parse(getBetweenStrings(html.data, 'ytcfg.set(', ');'));
|
||||||
if (data.INNERTUBE_CONTEXT) {
|
if (data.INNERTUBE_CONTEXT) {
|
||||||
this.key = data.INNERTUBE_API_KEY;
|
this.key = data.INNERTUBE_API_KEY;
|
||||||
this.context = data.INNERTUBE_CONTEXT;
|
this.context = data.INNERTUBE_CONTEXT;
|
||||||
this.context.client.clientName = "ANDROID";
|
this.context.client = constants.INNERTUBE_CLIENT(this.context.client)
|
||||||
this.context.client.clientVersion = "16.25";
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
if (this.checkErrorCallback) this.ErrorCallback(err, true)
|
|
||||||
if (this.retry_count >= 10) { this.init() } else { if (this.checkErrorCallback) this.ErrorCallback("Failed to retrieve Innertube session", true); }
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error) => error);
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
if (this.checkErrorCallback) this.ErrorCallback(err, true)
|
||||||
|
if (this.retry_count >= 10) { this.initAsync() } else { if (this.checkErrorCallback) this.ErrorCallback("Failed to retrieve Innertube session", true); }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.ErrorCallback(error, true)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
static create(ErrorCallback) {
|
static async createAsync(ErrorCallback) {
|
||||||
const created = new Innertube(ErrorCallback);
|
const created = new Innertube(ErrorCallback);
|
||||||
created.init();
|
await created.initAsync();
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
//--- API Calls ---//
|
//--- API Calls ---//
|
||||||
|
|
||||||
async browse(action_type) {
|
async browseAsync(action_type) {
|
||||||
let data = { context: this.context }
|
let data = { context: this.context }
|
||||||
|
|
||||||
switch (action_type) {
|
switch (action_type) {
|
||||||
|
@ -69,7 +69,7 @@ class Innertube {
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
}).catch((error) => error);
|
}).catch((error) => error);
|
||||||
|
|
||||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
if (response instanceof Error) return { success: false, status_code: response.status, message: response.message };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -78,13 +78,24 @@ class Innertube {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVidInfo(id) {
|
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`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVidInfoAsync(id) {
|
||||||
let data = { context: this.context, videoId: id }
|
let data = { context: this.context, videoId: id }
|
||||||
|
|
||||||
const response = await Http.post({
|
const response = await Http.post({
|
||||||
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
|
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
|
||||||
data: data,
|
data: data,
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: constants.INNERTUBE_HEADER(this.context)
|
||||||
}).catch((error) => error);
|
}).catch((error) => error);
|
||||||
|
|
||||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
|
||||||
|
@ -97,8 +108,10 @@ class Innertube {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple Wrappers
|
// Simple Wrappers
|
||||||
async getRecommendations() {
|
async getRecommendationsAsync() {
|
||||||
return await this.browse("recommendations")
|
const rec = await this.browseAsync("recommendations");
|
||||||
|
console.log(rec.data)
|
||||||
|
return rec.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -128,23 +128,33 @@ const searchModule = {
|
||||||
|
|
||||||
//--- Recommendations --//
|
//--- Recommendations --//
|
||||||
|
|
||||||
// Immediately create an Innertube object. This will be the object used in all future Inntertube API calls
|
let InnertubeAPI;
|
||||||
|
|
||||||
|
// Lazy loads Innertube object. This will be the object used in all future Innertube API calls. 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
|
// 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 recommendationModule = {
|
||||||
recommendAPI: Innertube.create((message, isError) => { logger("Innertube", message, isError); }), // There's definitely a better way to do this, but it's 2 am and I just can't anymore
|
|
||||||
|
async getAPI() {
|
||||||
|
if (!InnertubeAPI) {
|
||||||
|
InnertubeAPI = await Innertube.createAsync((message, isError) => { logger("Innertube", message, isError); })
|
||||||
|
}
|
||||||
|
return InnertubeAPI;
|
||||||
|
},
|
||||||
|
|
||||||
async getVid(id) {
|
async getVid(id) {
|
||||||
console.log(this.recommendAPI)
|
return InnertubeAPI.getVidInfoAsync(id).data;
|
||||||
return this.recommendAPI.getVidInfo(id);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async recommend() {
|
async recommend() {
|
||||||
return this.recommendAPI.getRecommendations();
|
return InnertubeAPI.getRecommendationsAsync();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getThumbnail: (id, resolution) => Innertube.getThumbnail(id, resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
//--- Start ---//
|
//--- Start ---//
|
||||||
export default ({ app }, inject) => {
|
export default ({ app }, inject) => {
|
||||||
inject('youtube', {...searchModule, ...recommendationModule })
|
inject('youtube', {...searchModule, ...recommendationModule, })
|
||||||
|
inject("logger", logger)
|
||||||
}
|
}
|
||||||
logger("Initialize", "Program Started");
|
logger("Initialize", "Program Started");
|
|
@ -1,11 +1,53 @@
|
||||||
// To centeralize certain values and URLs as for easier debugging and refactoring
|
// To centeralize certain values and URLs as for easier debugging and refactoring
|
||||||
|
|
||||||
|
const url = {
|
||||||
|
YT_URL: 'https://www.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",
|
||||||
|
VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube",
|
||||||
|
}
|
||||||
|
|
||||||
|
const ytApiVal = {
|
||||||
|
VERSION: "16.25",
|
||||||
|
CLIENTNAME: "ANDROID",
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
URLS: {
|
URLS: url,
|
||||||
YT_URL: 'https://www.youtube.com',
|
YT_API_VALUES: ytApiVal,
|
||||||
YT_MUSIC_URL: 'https://music.youtube.com',
|
|
||||||
YT_BASE_API: 'https://www.youtube.com/youtubei/v1',
|
INNERTUBE_HEADER: (info) => {
|
||||||
YT_SUGGESTIONS: "https://suggestqueries.google.com/complete",
|
let headers = {
|
||||||
VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube",
|
'accept': '*/*',
|
||||||
|
'user-agent': info.client.userAgent,
|
||||||
|
'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',
|
||||||
|
};
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
|
||||||
|
INNERTUBE_CLIENT: (info) => {
|
||||||
|
let client = {
|
||||||
|
"gl": info.gl,
|
||||||
|
"hl": info.hl,
|
||||||
|
"deviceMake": info.deviceMake,
|
||||||
|
"deviceModel": info.deviceModel,
|
||||||
|
"userAgent": info.userAgent,
|
||||||
|
"clientName": ytApiVal.CLIENTNAME,
|
||||||
|
"clientVersion": ytApiVal.VERSION,
|
||||||
|
"osName": info.osName,
|
||||||
|
"osVersion": info.osVersion,
|
||||||
|
"platform": "MOBILE",
|
||||||
|
"originalUrl": info.originalUrl,
|
||||||
|
"configInfo": info.configInfo,
|
||||||
|
"remoteHost": info.remoteHost,
|
||||||
|
"visitorData": info.visitorData,
|
||||||
|
// This is, by all accounts, a horrible implementation, but this is currently the only solution besides
|
||||||
|
};
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue