0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-23 11:45:15 +00:00

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

Reworked Video info to use the /watch and /next endpoint, reworked gridVideoRenderer render method
This commit is contained in:
Kenny 2022-03-23 08:48:03 -04:00 committed by GitHub
commit 9cb862a2b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 309 additions and 163 deletions

View file

@ -0,0 +1,46 @@
<template>
<v-card class="entry compactVideoRenderer" :to="`/watch?v=${video.id}`">
<v-card-text>
<div style="position: relative">
<v-img :src="video.thumbnail" />
<div
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.metadata.overlay[0]"
/>
</div>
<div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="parseBottom(video)" />
</v-card-text>
</v-card>
</template>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
}
.videoRuntimeFloat {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.5);
border-radius: 5px;
padding: 0px 4px 0px 4px;
}
</style>
<script>
export default {
props: {
video: Object,
},
methods: {
parseBottom(video) {
const bottomText = [video.channel, video.metadata.views];
if (video.metadata.published) bottomText.push(video.metadata.published);
return bottomText.join(" • ");
},
},
};
</script>

View file

@ -0,0 +1,113 @@
<template>
<v-card
class="entry gridVideoRenderer background"
:to="`/watch?v=${video.videoId}`"
flat
>
<div style="position: relative">
<v-img
:aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(video.videoId, 'max')"
/>
<div
class="videoRuntimeFloat"
:class="
'style-' +
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style
"
style="color: #fff"
v-text="
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text
.runs[0].text
"
/>
</div>
<div id="details">
<a
:href="
video.shortBylineText.runs[0].navigationEndpoint.browseEndpoint
.canonicalBaseUrl
"
class="avatar-link pt-2"
>
<v-img
class="avatar-thumbnail"
:src="video.channelThumbnail.thumbnails[0].url"
/>
</a>
<v-card-text class="pt-2">
<div
v-for="title in video.title.runs"
:key="title.text"
style="margin-top: 0.5em"
class="font-weight-medium vid-title"
>
{{ title.text }}
</div>
<div class="grey--text caption" v-text="parseBottom(video)" />
</v-card-text>
</div>
</v-card>
</template>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
}
.videoRuntimeFloat {
position: absolute;
bottom: 10px;
right: 10px;
border-radius: 5px;
padding: 0px 4px 0px 4px;
}
.videoRuntimeFloat.style-DEFAULT {
background: rgba(0, 0, 0, 0.5);
}
.videoRuntimeFloat.style-LIVE {
background: rgba(255, 0, 0, 0.5);
}
.vid-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
.avatar-thumbnail {
margin-top: 0.5rem;
margin-left: 0.5rem;
border-radius: 50%;
width: 35px;
height: 35px;
}
#details {
display: flex;
flex-direction: row;
flex-basis: auto;
}
</style>
<script>
export default {
props: ["video"],
methods: {
parseBottom(video) {
const bottomText = [
video.shortBylineText?.runs[0].text,
video.shortViewCountText?.runs[0].text,
];
if (video.publishedTimeText?.runs[0].text)
bottomText.push(video.publishedTimeText?.runs[0].text);
return bottomText.join(" · ");
},
},
};
</script>

View file

@ -6,42 +6,23 @@
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
<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
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.metadata.overlay[0]"
/>
</div>
<div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="parseBottom(video)" />
</v-card-text>
</v-card>
<v-list-item v-for="(video, index) in recommends" :key="index" class="pa-0">
<component
:is="Object.keys(video)[0]"
:key="video[Object.keys(video)[0]].videoId"
:video="video[Object.keys(video)[0]]"
></component>
</v-list-item>
</div>
</template>
<style scoped>
.entry {
margin-top: 1em;
width: 100%; /* Prevent Loading Weirdness */
}
.videoRuntimeFloat {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.5);
border-radius: 5px;
padding: 0 3px 0 3px;
}
</style>
<script>
import gridVideoRenderer from "./VideoRenderers/gridVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
},
props: {
recommends: Array,
},

View file

@ -1,13 +1,27 @@
<template>
<div>
<video controls autoplay :src="vidSrc" width="100%" style="max-height: 50vh" />
<video
controls
autoplay
:src="vidSrc"
width="100%"
style="max-height: 50vh"
/>
<v-card v-if="loaded" class="ml-2 mr-2 background" flat>
<v-card-title class="mt-2"
style="padding-top: 0; padding-bottom: 0; font-size: 0.95rem; line-height: 1rem;"
<v-card-title
class="mt-2"
style="
padding-top: 0;
padding-bottom: 0;
font-size: 0.95rem;
line-height: 1rem;
"
v-text="title"
/>
<v-card-text>
<div style="margin-bottom: 1rem;">{{ views }} views {{ uploaded }}</div>
<div style="margin-bottom: 1rem">
{{ views }} views {{ uploaded }}
</div>
<!-- Scrolling Div For Interactions --->
<div style="display: flex; margin-bottom: 1em">
@ -25,7 +39,11 @@
@click="callMethodByName(item.actionName)"
>
<v-icon v-text="item.icon" />
<div class="mt-2" style="font-size: .66rem;" v-text="item.value || item.name" />
<div
class="mt-2"
style="font-size: 0.66rem"
v-text="item.value || item.name"
/>
</v-btn>
</v-list-item>
@ -131,7 +149,9 @@ export default {
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
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();
@ -140,11 +160,7 @@ export default {
this.interactions[0].value = result.metadata.likes;
this.loaded = true;
this.recommends = this.$youtube
.viewRecommends(result.renderedData.recommendations)
.filter((element) => {
return element !== undefined;
});
this.recommends = result.renderedData.recommendations;
// .catch((error) => this.$logger("Watch", error, true));
console.log("recommendations:", this.recommends);
});
@ -165,10 +181,10 @@ export default {
await Share.share({
title: this.title,
text: this.title,
url: 'https://youtu.be/' + this.$route.query.v,
dialogTitle: 'Share video',
url: "https://youtu.be/" + this.$route.query.v,
dialogTitle: "Share video",
});
}
},
},
watch: {
// Watch for change in the route query string (in this case, ?v=xxxxxxxx to ?v=yyyyyyyy)
@ -180,8 +196,8 @@ export default {
this.vidSrc = "";
this.getVideo();
}
}
}
}
},
},
},
};
</script>

View file

@ -39,8 +39,6 @@ module.exports = {
"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;
},

View file

@ -109,33 +109,35 @@ class Innertube {
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,
}),
const responseNext = await Http.post({
url: `${constants.URLS.YT_BASE_API}/next?v=${id}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context.client),
}).catch((error) => error);
const responseMobile = await Http.post({
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context),
headers: constants.INNERTUBE_HEADER(this.context.client),
}).catch((error) => error);
if (response instanceof Error)
if (response.error)
return {
success: false,
status_code: response.response.status,
status_code: response.status,
message: response.message,
};
}
else if (responseNext.error)
return {
success: false,
status_code: responseNext.status,
message: responseNext.message,
}
return {
success: true,
status_code: response.status,
data: { webOutput: response.data, appOutput: responseMobile.data },
data: { output: response.data, outputNext: responseNext.data },
};
}
@ -150,68 +152,57 @@ class Innertube {
let response = await this.getVidAsync(id);
if (
response.success &&
response.data.webOutput[2].playerResponse?.playabilityStatus?.status ==
response.success == false ||
response.data.output?.playabilityStatus?.status ==
("ERROR" || undefined)
)
throw new Error(
`Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`
`Could not get information for video: ${response.status_code || response.data.output?.playabilityStatus?.status} - ${response.message || response.data.output?.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;
const responseInfo = response.data.output;
const responseNext = response.data.outputNext;
const details = responseInfo.videoDetails;
// const columnUI =
// responseInfo[3].response?.contents.singleColumnWatchNextResults?.results
// ?.results;
const resolutions = responseInfo.streamingData;
const columnUI = responseNext.contents.singleColumnWatchNextResults.results.results
console.log(columnUI.contents.length);
return {
const vidData = {
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,
title: details.title,
isLive: details.isLiveContent,
channelName: details.author,
channelUrl: columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(contents => contents.elementRenderer)?.newElement?.type?.componentType?.model?.channelBarModel?.videoChannelBarData
?.onTap?.innertubeCommand?.browseEndpoint?.canonicalBaseUrl,
channelImg: columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(contents => contents.elementRenderer)?.newElement?.type?.componentType?.model?.channelBarModel?.videoChannelBarData
?.avatar?.image?.sources[0].url,
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,
description: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails,
uploadDate: columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(contents => contents.slimVideoDescriptionRenderer)?.slimVideoDescriptionRenderer.publishDate.runs[0].text,
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.
viewCount: details.viewCount,
lengthSeconds: details.lengthSeconds,
likes: parseInt(columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(contents => contents.slimVideoScrollableActionBarRenderer)?.slimVideoScrollableActionBarRenderer
.buttons.find(button => button.slimMetadataToggleButtonRenderer.isLike == true)?.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,
description: columnUI?.contents.find(contents => contents.slimVideoMetadataSectionRenderer).slimVideoMetadataSectionRenderer?.contents.find(contents => contents.slimVideoDescriptionRenderer)?.slimVideoDescriptionRenderer.description.runs,
recommendations: columnUI?.contents.find(contents => contents.shelfRenderer).shelfRenderer?.content?.horizontalListRenderer?.items,
recommendationsContinuation: columnUI?.continuations[0].reloadContinuationData?.continuation
},
};
console.log(vidData)
return vidData
}
}
export default Innertube;

View file

@ -10,6 +10,8 @@ function useRender(video, renderer) {
return gridVideoRenderer(video);
case "compactAutoplayRenderer":
return compactAutoplayRenderer(video);
case "compactVideoRenderer":
return compactVideoRenderer(video);
default:
return undefined;
}
@ -49,35 +51,6 @@ function gridVideoRenderer(video) {
};
}
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;
@ -86,4 +59,29 @@ function compactAutoplayRenderer(video) {
else return undefined;
}
function compactVideoRenderer(video) {
return {
id: video.videoId,
title: video.title?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0].text,
channelURL:
video.shortBylineText?.runs[0].navigationEndpoint?.browseEndpoint
?.canonicalBaseUrl,
channelThumbnail: video.channelThumbnail?.thumbnails[0].url,
metadata: {
views: video.viewCountText?.runs[0].text,
length: video.lengthText?.runs[0].text,
publishedTimeText: video.publishedTimeText.runs[0].text,
overlayStyle: video.thumbnailOverlays?.map(
(overlay) => overlay.thumbnailOverlayTimeStatusRenderer?.style
),
overlay: video.thumbnailOverlays?.map(
(overlay) =>
overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text
),
},
};
}
export default useRender;

View file

@ -212,6 +212,18 @@ const innertubeModule = {
}
},
getThumbnail(id, resolution) {
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`;
},
// It just works™
// Front page recommendation
async recommend() {
@ -226,35 +238,26 @@ const innertubeModule = {
const video =
shelves.shelfRenderer?.content?.horizontalListRenderer?.items;
if (video)
return video.map((item) => {
if (item) {
const renderedItem = useRender(
item[Object.keys(item)[0]],
Object.keys(item)[0]
);
console.log(renderedItem);
return renderedItem;
} else {
return undefined;
}
});
if (video) return video;
// if (video)
// return video.map((item) => {
// 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;
},
// 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 ---//