0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-26 13:13:03 +00:00

refactor: better searching using the innertube API

This commit is contained in:
Alex 2022-03-24 02:07:03 +13:00
parent d4a09024e4
commit 403a8535e9
7 changed files with 236 additions and 113 deletions

View file

@ -1,17 +1,53 @@
<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]"
<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"
/>
</div>
<div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="parseBottom(video)" />
</v-card-text>
</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>
@ -23,23 +59,54 @@
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.5);
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: Object,
},
props: ["video"],
methods: {
parseBottom(video) {
const bottomText = [video.channel, video.metadata.views];
if (video.metadata.published) bottomText.push(video.metadata.published);
return bottomText.join(" • ");
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(" · ");
},
},
};

View file

@ -8,6 +8,7 @@
<v-list-item v-for="(video, index) in recommends" :key="index" class="pa-0">
<component
v-if="getComponents()[Object.keys(video)[0]]"
:is="Object.keys(video)[0]"
:key="video[Object.keys(video)[0]].videoId"
:video="video[Object.keys(video)[0]]"
@ -17,11 +18,13 @@
</template>
<script>
import compactVideoRenderer from "./VideoRenderers/compactVideoRenderer.vue";
import gridVideoRenderer from "./VideoRenderers/gridVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
compactVideoRenderer,
},
props: {
recommends: Array,
@ -33,6 +36,10 @@ export default {
if (video.metadata.published) bottomText.push(video.metadata.published);
return bottomText.join(" • ");
},
getComponents() {
return this.$options.components;
},
},
};
</script>

View file

@ -4,11 +4,13 @@
* This is to allow use of "recommended" videos on other pages such as /watch
* -Front
* -->
<recommended :recommends="recommends" />
<horizontal-list-renderer :recommends="recommends" />
</template>
<script>
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
export default {
components: { horizontalListRenderer },
data() {
return {
recommends: [],

View file

@ -5,22 +5,7 @@
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
<v-list-item v-for="(video, index) in videos" :key="index" class="pa-0">
<v-card class="entry background" :to="`/watch?v=${video.id}`" flat>
<div style="position: relative">
<v-img :src="video.thumbnails[video.thumbnails.length - 1].url" />
<div
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.runtime"
/>
</div>
<div class="px-4 pt-4" v-text="video.title" />
<v-card-text class="pt-0">
<div v-text="`${video.views} • ${video.uploaded}`" />
</v-card-text>
</v-card>
</v-list-item>
<horizontal-list-renderer :recommends="videos" />
</div>
</template>
@ -39,7 +24,9 @@
</style>
<script>
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
export default {
components: { horizontalListRenderer },
data() {
return {
videos: [],
@ -51,9 +38,8 @@ export default {
methods: {
getSearch() {
const searchQuestion = this.$route.query.q;
const vm = this;
this.$youtube.search(searchQuestion, (data) => {
vm.videos = data;
this.$youtube.search(searchQuestion).then((response) => {
this.videos = response.items;
});
},
},

View file

@ -85,7 +85,7 @@
</v-bottom-sheet> -->
</v-card>
<recommended :recommends="recommends" />
<horizontal-list-renderer :recommends="recommends" />
</div>
</template>
@ -98,8 +98,10 @@
<script>
import { Share } from "@capacitor/share";
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
export default {
components: { horizontalListRenderer },
data() {
return {
interactions: [

View file

@ -95,18 +95,6 @@ class Innertube {
};
}
static 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`;
}
async getVidAsync(id) {
let data = { context: this.context, videoId: id };
const responseNext = await Http.post({
@ -121,19 +109,19 @@ class Innertube {
headers: constants.INNERTUBE_HEADER(this.context.client),
}).catch((error) => error);
if (response.error)
if (response.error)
return {
success: false,
status_code: response.status,
message: response.message,
}
else if (responseNext.error)
};
else if (responseNext.error)
return {
success: false,
status_code: responseNext.status,
message: responseNext.message,
}
};
return {
success: true,
status_code: response.status,
@ -141,6 +129,43 @@ class Innertube {
};
}
async searchAsync(query) {
let data = { context: this.context, query: query };
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/search?key=${this.key}`,
data: data,
headers: { "Content-Type": "application/json" },
}).catch((error) => error);
if (response instanceof Error)
return {
success: false,
status_code: response.status,
message: response.message,
};
return {
success: true,
status_code: response.status,
data: response.data,
};
}
// Static methods
static 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`;
}
// Simple Wrappers
async getRecommendationsAsync() {
const rec = await this.browseAsync("recommendations");
@ -153,56 +178,96 @@ class Innertube {
if (
response.success == false ||
response.data.output?.playabilityStatus?.status ==
("ERROR" || undefined)
response.data.output?.playabilityStatus?.status == ("ERROR" || undefined)
)
throw new Error(
`Could not get information for video: ${response.status_code || response.data.output?.playabilityStatus?.status} - ${response.message || response.data.output?.playabilityStatus?.reason}`
`Could not get information for video: ${
response.status_code ||
response.data.output?.playabilityStatus?.status
} - ${
response.message || response.data.output?.playabilityStatus?.reason
}`
);
const responseInfo = response.data.output;
const responseNext = response.data.outputNext;
const details = responseInfo.videoDetails;
// const columnUI =
// responseInfo[3].response?.contents.singleColumnWatchNextResults?.results
// ?.results;
// ?.results;
const resolutions = responseInfo.streamingData;
const columnUI = responseNext.contents.singleColumnWatchNextResults.results.results
const columnUI =
responseNext.contents.singleColumnWatchNextResults.results.results;
const vidData = {
id: details.videoId,
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,
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: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails,
uploadDate: columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(contents => contents.slimVideoDescriptionRenderer)?.slimVideoDescriptionRenderer.publishDate.runs[0].text,
uploadDate:
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(
(contents) => contents.slimVideoDescriptionRenderer
)?.slimVideoDescriptionRenderer.publishDate.runs[0].text,
isPrivate: details.isPrivate,
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.
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: 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
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)
console.log(vidData);
return vidData
return vidData;
}
async getSearchAsync(query) {
const search = await this.searchAsync(query);
if (search.success == false)
throw new Error(
`Could not get search results: ${search.status_code} - ${search.message}`
);
console.log(search.data);
return search.data.contents.sectionListRenderer.contents.find(
(contents) => contents.shelfRenderer
).shelfRenderer;
}
}
export default Innertube;

View file

@ -111,30 +111,30 @@ const searchModule = {
});
},
search(text, callback) {
let results = new Array();
youtubeSearch(text, (videos) => {
for (const i in videos) {
const video = videos[i];
// search(text, callback) {
// 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(constants.LOGGER_NAMES.search, { type: "Error Caught Successfully", error: video }, true);
}
}
});
callback(results);
},
// 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(constants.LOGGER_NAMES.search, { type: "Error Caught Successfully", error: video }, true);
// }
// }
// });
// callback(results);
// },
getRemainingVideoInfo(id, callback) {
String.prototype.decodeEscapeSequence = function () {
@ -239,25 +239,19 @@ const innertubeModule = {
shelves.shelfRenderer?.content?.horizontalListRenderer?.items;
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
async search(query) {
try {
const response = await InnertubeAPI.getSearchAsync(query);
return response.content.verticalListRenderer;
} catch (err) {
logger(constants.LOGGER_NAMES.search, err, true);
}
},
};
//--- Start ---//