0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-25 12:45:17 +00:00

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

Feat: Groundwork for Innertube wrapper, plus a bit of code cleanup
This commit is contained in:
Sushi 2022-03-17 07:55:02 -07:00 committed by GitHub
commit 3fae34492d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 296 additions and 128 deletions

View file

@ -1,7 +1,24 @@
// Buttons and methods for testing and demonstration purposes only. Uncomment them to see how it works. Remove to actually implement a implementation
<template>
<center>
<img style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" />
<h1 class="grey--text">Page Under Construction</h1>
<p class="grey--text">Please read the VueTube FAQ for more information.</p>
<!-- <button @click="debugRecommend">Test Button</button>
<button @click="debugVideo">Test Button (Video)</button> -->
</center>
</template>
<script>
export default {
methods: {
// debugRecommend () {
// console.log(this.$youtube.recommend("test", false))
// },
// debugVideo () {
// console.log(this.$youtube.getVid("WhWc3b3KhnY"))
// }
}
}
</script>

107
NUXT/plugins/innertube.js Normal file
View file

@ -0,0 +1,107 @@
// Code specific to working with the innertube API
// https://www.youtube.com/youtubei/v1
import { Http } from '@capacitor-community/http';
import { getBetweenStrings } from './utils';
import constants from '../static/constants';
class Innertube {
//--- Initiation ---//
constructor(ErrorCallback) {
this.ErrorCallback = ErrorCallback || undefined;
this.retry_count = 0
}
checkErrorCallback() {
return typeof this.ErrorCallback === "function"
}
init() {
Http.get({ url: constants.URLS.YT_URL, params: { hl: "en" } })
.then(result => {
if (result instanceof Error && this.checkErrorCallback) this.ErrorCallback(result.message, true);
try {
const data = JSON.parse(getBetweenStrings(result.data, 'ytcfg.set(', ');'));
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.context = data.INNERTUBE_CONTEXT;
this.context.client.clientName = "ANDROID";
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);
};
static create(ErrorCallback) {
const created = new Innertube(ErrorCallback);
created.init();
return created;
}
//--- API Calls ---//
async browse(action_type) {
let data = { context: this.context }
switch (action_type) {
case 'recommendations':
data.browseId = 'FEwhat_to_watch'
break;
case 'playlist':
data.browseId = args.browse_id
break;
default:
}
console.log(data)
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`,
data: data,
headers: { "Content-Type": "application/json" }
}).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
success: true,
status_code: response.status,
data: response.data
};
}
async getVidInfo(id) {
let data = { context: this.context, videoId: id }
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: { "Content-Type": "application/json" }
}).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
success: true,
status_code: response.status,
data: response.data
};
}
// Simple Wrappers
async getRecommendations() {
return await this.browse("recommendations")
}
}
export default Innertube;

13
NUXT/plugins/utils.js Normal file
View file

@ -0,0 +1,13 @@
// Collection of functions that are useful but non-specific to any particular files
function getBetweenStrings(data, start_string, end_string) {
const regex = new RegExp(`${escapeRegExp(start_string)}(.*?)${escapeRegExp(end_string)}`, "s");
const match = data.match(regex);
return match ? match[1] : undefined;
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
module.exports = { getBetweenStrings };

View file

@ -1,46 +1,47 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import constants from '../static/constants';
const module = {
//--- Get GitHub Commits ---//
commits: new Promise((resolve, reject) => {
//--- Get GitHub Commits ---//
commits: new Promise((resolve, reject) => {
Http.request({
method: 'GET',
url: "https://api.github.com/repos/Frontesque/VueTube/commits",
params: { }
})
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err)
});
Http.request({
method: 'GET',
url: `${constants.URLS.VT_GITHUB}/commits`,
params: {}
})
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err)
});
}),
}),
getRuns(item, callback) {
getRuns(item, callback) {
let url = `https://api.github.com/repos/Frontesque/VueTube/commits/${item.sha}/check-runs`;
let url = `${constants.URLS.VT_GITHUB}/commits/${item.sha}/check-runs`;
Http.request({
method: 'GET',
url: url,
params: { }
})
.then((res) => {
callback(res.data)
})
.catch((err) => {
callback(err)
});
Http.request({
method: 'GET',
url: url,
params: {}
})
.then((res) => {
callback(res.data)
})
.catch((err) => {
callback(err)
});
}
}
}
//--- Start ---//
export default ({ app }, inject) => {
inject('vuetube', module)
}
inject('vuetube', module)
}

View file

@ -1,131 +1,150 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import Innertube from './innertube'
import constants from '../static/constants';
//--- Logger Function ---//
function logger(func, data, isError=false) {
module.logs.unshift({
name: func,
time: Date.now(),
data: data,
error: isError
})
function logger(func, data, isError = false) {
searchModule.logs.unshift({
name: func,
time: Date.now(),
data: data,
error: isError
})
}
//--- Youtube Base Parser ---//
function youtubeParse(html, callback) {
//--- Replace Encoded Characters ---///
html = html.replace(/\\x([0-9A-F]{2})/ig, (...items) => { return String.fromCharCode(parseInt(items[1], 16)); });
//--- Properly Format JSON ---//
html = html.replaceAll("\\\\\"", "");
//--- Parse JSON ---//
html = JSON.parse(html);
//--- Replace Encoded Characters ---///
html = html.replace(/\\x([0-9A-F]{2})/ig, (...items) => { return String.fromCharCode(parseInt(items[1], 16)); });
//--- Properly Format JSON ---//
html = html.replaceAll("\\\\\"", "");
//--- Parse JSON ---//
html = JSON.parse(html);
//--- Get Results ---// ( Thanks To appit-online On Github ) -> https://github.com/appit-online/youtube-search/blob/master/src/lib/search.ts
let results;
if (html && html.contents && html.contents.sectionListRenderer && html.contents.sectionListRenderer.contents
&& html.contents.sectionListRenderer.contents.length > 0
&& html.contents.sectionListRenderer.contents[0].itemSectionRenderer
&& html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents.length > 0) {
results = html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
logger("search", results);
callback(results);
} else {
try {
results = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]);
logger("search", results);
callback(results);
} catch (e) {}
try {
results = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents;
logger("search", results);
callback(results);
} catch(e) {}
}
//--- Get Results ---// ( Thanks To appit-online On Github ) -> https://github.com/appit-online/youtube-search/blob/master/src/lib/search.ts
let results;
if (html && html.contents && html.contents.sectionListRenderer && html.contents.sectionListRenderer.contents &&
html.contents.sectionListRenderer.contents.length > 0 &&
html.contents.sectionListRenderer.contents[0].itemSectionRenderer &&
html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents.length > 0) {
results = html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents;
logger("search", results);
callback(results);
} else {
try {
results = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]);
logger("search", results);
callback(results);
} catch (e) {}
try {
results = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents;
logger("search", results);
callback(results);
} catch (e) {}
}
}
//--- Search Main Function ---//
function youtubeSearch(text, callback) {
Http.request({
method: 'GET',
url: 'https://youtube.com/results',
params: { q: text, hl: "en" }
})
.then((res) => {
//--- Get HTML Only ---//
let html = res.data;
//--- Isolate The Script Containing Video Information ---//
html = html.split("var ytInitialData = '")[1].split("';</script>")[0];
Http.request({
method: 'GET',
url: `${constants.URLS.YT_URL}/results`,
params: { q: text, hl: "en" }
})
.then((res) => {
//--- Get HTML Only ---//
let html = res.data;
//--- Isolate The Script Containing Video Information ---//
html = html.split("var ytInitialData = '")[1].split("';</script>")[0];
youtubeParse(html, (data) => {
callback(data);
})
youtubeParse(html, (data) => {
callback(data);
})
})
.catch((err) => {
logger("search", err, true);
callback(err);
});
})
.catch((err) => {
logger("search", err, true);
callback(err);
});
}
const module = {
logs: new Array(),
const searchModule = {
logs: new Array(),
//--- Get YouTube's Search Auto Complete ---//
autoComplete(text, callback) {
Http.request({
method: 'GET',
url: 'https://suggestqueries-clients6.youtube.com/complete/search',
params: { client: 'youtube', q: text }
})
.then((res) => {
logger("autoComplete", res);
callback(res.data);
})
.catch((err) => {
logger("autoComplete", err, true);
callback(err);
});
},
//--- Get YouTube's Search Auto Complete ---//
autoComplete(text, callback) {
Http.request({
method: 'GET',
url: `${constants.URLS.YT_SUGGESTIONS}/search`,
params: { client: 'youtube', q: text }
})
.then((res) => {
logger("autoComplete", res);
callback(res.data);
})
.catch((err) => {
logger("autoComplete", err, true);
callback(err);
});
},
search(text, callback) {
search(text, callback) {
let results = new Array();
youtubeSearch(text, (videos) => {
for (const i in videos) {
const video = videos[i];
let results = new Array();
youtubeSearch(text, (videos) => {
for (const i in videos) {
const video = videos[i];
if (video.compactVideoRenderer) {
//--- If Entry Is A Video ---//
results.push({
id: video.compactVideoRenderer.videoId,
title: video.compactVideoRenderer.title.runs[0].text,
runtime: video.compactVideoRenderer.lengthText.runs[0].text,
uploaded: video.compactVideoRenderer.publishedTimeText.runs[0].text,
views: video.compactVideoRenderer.viewCountText.runs[0].text,
thumbnails: video.compactVideoRenderer.thumbnail.thumbnails
})
} else {
//--- If Entry Is Not A Video ---//
//logger("search", { type: "Error Caught Successfully", error: video }, true);
}
if (video.compactVideoRenderer) {
//--- If Entry Is A Video ---//
results.push({
id: video.compactVideoRenderer.videoId,
title: video.compactVideoRenderer.title.runs[0].text,
runtime: video.compactVideoRenderer.lengthText.runs[0].text,
uploaded: video.compactVideoRenderer.publishedTimeText.runs[0].text,
views: video.compactVideoRenderer.viewCountText.runs[0].text,
thumbnails: video.compactVideoRenderer.thumbnail.thumbnails
})
} else {
//--- If Entry Is Not A Video ---//
//logger("search", { type: "Error Caught Successfully", error: video }, true);
}
}
})
callback(results);
}
})
callback(results);
},
},
getVideo(id) {
return id;
}
getVideo(id) {
return id;
}
}
//--- Recommendations --//
// Immediately create an Innertube object. This will be the object used in all future Inntertube API calls
// 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 = {
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 getVid(id) {
console.log(this.recommendAPI)
return this.recommendAPI.getVidInfo(id);
},
async recommend() {
return this.recommendAPI.getRecommendations();
},
}
//--- Start ---//
export default ({ app }, inject) => {
inject('youtube', module)
inject('youtube', {...searchModule, ...recommendationModule })
}
logger("Initialize","Program Started");
logger("Initialize", "Program Started");

11
NUXT/static/constants.js Normal file
View file

@ -0,0 +1,11 @@
// To centeralize certain values and URLs as for easier debugging and refactoring
module.exports = {
URLS: {
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",
}
}