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:
commit
3fae34492d
6 changed files with 296 additions and 128 deletions
|
@ -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
107
NUXT/plugins/innertube.js
Normal 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
13
NUXT/plugins/utils.js
Normal 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 };
|
|
@ -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)
|
||||
}
|
|
@ -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
11
NUXT/static/constants.js
Normal 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",
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue