diff --git a/NUXT/components/recommended.vue b/NUXT/components/recommended.vue
index 3f01afa..2cd825e 100644
--- a/NUXT/components/recommended.vue
+++ b/NUXT/components/recommended.vue
@@ -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
-
@@ -44,22 +42,8 @@
\ No newline at end of file
diff --git a/NUXT/pages/index.vue b/NUXT/pages/index.vue
index 1d851ce..420f451 100644
--- a/NUXT/pages/index.vue
+++ b/NUXT/pages/index.vue
@@ -1,12 +1,8 @@
-
-
-
-
+
+
+
+
diff --git a/NUXT/pages/watch.vue b/NUXT/pages/watch.vue
index 83aeb75..f9b067c 100644
--- a/NUXT/pages/watch.vue
+++ b/NUXT/pages/watch.vue
@@ -2,66 +2,73 @@
-
+
- {{ views }} views • {{uploaded}}
-
-
-
-
+ {{ views }} views • {{ uploaded }}
-
-
+
+
+
+
-
-
mdi-chevron-up
mdi-chevron-down
-
-
+
Channel Stuff
-
-
-
+
-
- {{ description }}
-
+
+ {{ description }}
+
-
-
-
-
-
-
- {{ description }}
-
-
+
-
-
+
@@ -74,23 +81,36 @@
diff --git a/NUXT/static/constants.js b/NUXT/plugins/constants.js
similarity index 71%
rename from NUXT/static/constants.js
rename to NUXT/plugins/constants.js
index 04a6b8e..3cac6a1 100644
--- a/NUXT/static/constants.js
+++ b/NUXT/plugins/constants.js
@@ -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
- }
-}
\ No newline at end of file
+ },
+}
diff --git a/NUXT/plugins/innertube.js b/NUXT/plugins/innertube.js
index 77b836a..b271fef 100644
--- a/NUXT/plugins/innertube.js
+++ b/NUXT/plugins/innertube.js
@@ -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;
\ No newline at end of file
+export default Innertube
diff --git a/NUXT/plugins/renderers.js b/NUXT/plugins/renderers.js
new file mode 100644
index 0000000..9bb0744
--- /dev/null
+++ b/NUXT/plugins/renderers.js
@@ -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
\ No newline at end of file
diff --git a/NUXT/plugins/ryd.js b/NUXT/plugins/ryd.js
index 069bb4d..21f903b 100644
--- a/NUXT/plugins/ryd.js
+++ b/NUXT/plugins/ryd.js
@@ -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 =
diff --git a/NUXT/plugins/vuetube.js b/NUXT/plugins/vuetube.js
index 91a61d0..285ace9 100644
--- a/NUXT/plugins/vuetube.js
+++ b/NUXT/plugins/vuetube.js
@@ -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 = {
diff --git a/NUXT/plugins/youtube.js b/NUXT/plugins/youtube.js
index 88af576..d770c75 100644
--- a/NUXT/plugins/youtube.js
+++ b/NUXT/plugins/youtube.js
@@ -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");