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:
parent
d4a09024e4
commit
403a8535e9
7 changed files with 236 additions and 113 deletions
|
@ -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(" · ");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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: [],
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ---//
|
||||
|
|
Loading…
Reference in a new issue