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

fixed issue with thumbnail quality, refactored file structure for rendering, fixed search
This commit is contained in:
Kenny 2022-03-24 16:35:48 -04:00 committed by GitHub
commit 2adafc2029
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 359 additions and 212 deletions

View File

@ -0,0 +1,88 @@
<template>
<v-card
class="entry gridVideoRenderer background"
:to="`/watch?v=${video.videoId}`"
flat
>
<div id="details">
<a
:href="video.navigationEndpoint.browseEndpoint.canonicalBaseUrl"
class="avatar-link pt-2"
>
<v-img
class="avatar-thumbnail"
:src="
video.thumbnail.thumbnails[video.thumbnail.thumbnails.length - 1]
.url
"
/>
</a>
<v-card-text class="video-info pt-2">
<div
v-for="title in video.title.runs"
:key="title.text"
style="margin-top: 0.5em"
class="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 */
}
.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: 50px;
height: 50px;
}
#details {
display: flex;
flex-direction: row;
flex-basis: auto;
padding: 10px;
}
@media screen and (orientation: landscape) {
.entry {
margin-bottom: 8px;
}
#details {
flex-direction: column-reverse;
}
}
</style>
<script>
export default {
props: ["video"],
methods: {
parseBottom(video) {
const bottomText = [
video.subscriberCountText?.runs[0].text,
video.videoCountText?.runs.map((run) => run.text).join(" "),
];
return bottomText.join(" · ");
},
},
};
</script>

View File

@ -7,7 +7,13 @@
<div style="position: relative" class="thumbnail-container">
<v-img
:aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(video.videoId, 'max')"
:src="
$youtube.getThumbnail(
video.videoId,
'max',
video.thumbnail.thumbnails
)
"
/>
<div
class="videoRuntimeFloat"

View File

@ -0,0 +1,39 @@
<template>
<div>
<v-list-item
v-for="(video, index) in render.items"
: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]]"
></component>
</v-list-item>
</div>
</template>
<script>
import compactVideoRenderer from "../CompactRenderers/compactVideoRenderer.vue";
import gridVideoRenderer from "../GridRenderers/gridVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
compactVideoRenderer,
},
props: ["render"],
methods: {
getComponents() {
return this.$options.components;
},
},
mounted() {
console.log("horizontalListRenderer received: ", this.render);
},
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<div>
<v-list-item
v-for="(renderer, index) in render.contents"
:key="index"
class="pa-0"
>
<component
v-if="getComponents()[Object.keys(renderer)[0]]"
:is="Object.keys(renderer)[0]"
:key="index"
:render="renderer[Object.keys(renderer)[0]]"
></component>
</v-list-item>
</div>
</template>
<script>
import itemSectionRenderer from "../SectionRenderers/itemSectionRenderer.vue";
import shelfRenderer from "../SectionRenderers/shelfRenderer.vue";
export default {
components: {
itemSectionRenderer,
shelfRenderer,
},
props: ["render"],
methods: {
getComponents() {
return this.$options.components;
},
},
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<div>
<v-list-item
v-for="(video, index) in render.items"
: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]]"
></component>
</v-list-item>
</div>
</template>
<script>
import gridVideoRenderer from "../GridRenderers/gridVideoRenderer.vue";
import compactVideoRenderer from "../CompactRenderers/compactVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
compactVideoRenderer,
},
props: ["render"],
methods: {
getComponents() {
return this.$options.components;
},
},
};
</script>

View File

@ -0,0 +1,55 @@
<template>
<div>
<div
v-for="(video, index) in render.contents"
:key="index"
class="pa-0 fill-screen"
>
<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]]"
></component>
</div>
<div
v-if="
render.separatorDetails && render.separatorDetails.hasBottomSeparator
"
class="separator-bottom grey"
:style="{ height: render.separatorDetails.height + 'px' }"
></div>
</div>
</template>
<style scoped>
.shelf-header {
width: 100%; /* Prevent Loading Weirdness */
padding: 10px;
}
.fill-screen {
widows: 100 vw; /* Very Hacky */
}
</style>
<script>
import compactVideoRenderer from "../CompactRenderers/compactVideoRenderer.vue";
import compactChannelRenderer from "../CompactRenderers/compactChannelRenderer.vue";
import gridVideoRenderer from "../GridRenderers/gridVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
compactVideoRenderer,
compactChannelRenderer,
},
props: ["render"],
methods: {
getComponents() {
return this.$options.components;
},
},
};
</script>

View File

@ -0,0 +1,46 @@
<template>
<div>
<h4 v-if="render.headerRenderer" class="font-weight-bold shelf-header">
{{
render.headerRenderer.elementRenderer.newElement.type.componentType
.model.shelfHeaderModel.shelfHeaderData.title
}}
</h4>
<component
v-if="render.content && getComponents()[Object.keys(render.content)[0]]"
:is="Object.keys(render.content)[0]"
:render="render.content[Object.keys(render.content)[0]]"
></component>
<div
v-if="render.separator && render.separator.hasBottomSeparator"
class="separator-bottom grey"
:style="{ height: render.separator.height + 'px' }"
></div>
</div>
</template>
<style scoped>
.shelf-header {
width: 100%; /* Prevent Loading Weirdness */
padding: 10px;
}
</style>
<script>
import verticalListRenderer from "../ListRenderers/verticalListRenderer.vue";
import horizontalListRenderer from "../ListRenderers/horizontalListRenderer.vue";
export default {
components: {
horizontalListRenderer,
verticalListRenderer,
},
props: ["render"],
methods: {
getComponents() {
return this.$options.components;
},
},
};
</script>

View File

@ -7,7 +7,13 @@
<div style="position: relative" class="thumbnail-container">
<v-img
:aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(video.videoId, 'max')"
:src="
$youtube.getThumbnail(
video.videoId,
'max',
video.thumbnail.thumbnails
)
"
/>
<div
class="videoRuntimeFloat"
@ -131,5 +137,9 @@ export default {
return bottomText.join(" · ");
},
},
mounted() {
console.log("gridVideoRenderer received: ", this.video);
},
};
</script>

View File

@ -1,45 +0,0 @@
<template>
<div>
<!-- Video Loading Animation -->
<center v-if="recommends.length == 0">
<v-skeleton-loader type="card-avatar, article, actions" />
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
<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]]"
></component>
</v-list-item>
</div>
</template>
<script>
import compactVideoRenderer from "./VideoRenderers/compactVideoRenderer.vue";
import gridVideoRenderer from "./VideoRenderers/gridVideoRenderer.vue";
export default {
components: {
gridVideoRenderer,
compactVideoRenderer,
},
props: {
recommends: Array,
},
methods: {
parseBottom(video) {
const bottomText = [video.channel, video.metadata.views];
if (video.metadata.published) bottomText.push(video.metadata.published);
return bottomText.join(" • ");
},
getComponents() {
return this.$options.components;
},
},
};
</script>

View File

@ -0,0 +1,7 @@
// this is an loading animation for videos
<template>
<center>
<v-skeleton-loader type="card-avatar, article, actions" />
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
</template>

View File

@ -4,13 +4,19 @@
* This is to allow use of "recommended" videos on other pages such as /watch
* -Front
* -->
<horizontal-list-renderer :recommends="recommends" class="video-list" />
<div>
<!-- Video Loading Animation -->
<vid-load-renderer v-if="!recommends" />
<horizontal-list-renderer v-else :render="recommends" />
</div>
</template>
<script>
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
import horizontalListRenderer from "../components/ListRenderers/horizontalListRenderer.vue";
import VidLoadRenderer from "../components/vidLoadRenderer.vue";
export default {
components: { horizontalListRenderer },
components: { horizontalListRenderer, VidLoadRenderer },
computed: {
recommends: {

View File

@ -1,11 +1,8 @@
<template>
<div class="accent">
<center v-if="videos.length == -1">
<v-skeleton-loader type="card-avatar, article, actions" />
<v-skeleton-loader type="card-avatar, article, actions" />
</center>
<horizontal-list-renderer :recommends="videos" />
<!-- Video Loading Animation -->
<vid-load-renderer v-if="renderer.length <= 0" />
<sectionListRenderer :render="renderer" />
</div>
</template>
@ -24,12 +21,17 @@
</style>
<script>
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
import sectionListRenderer from "../components/ListRenderers/sectionListRenderer.vue";
import VidLoadRenderer from "../components/vidLoadRenderer.vue";
export default {
components: { horizontalListRenderer },
components: {
sectionListRenderer,
VidLoadRenderer,
},
data() {
return {
videos: [],
renderer: [],
};
},
mounted() {
@ -39,7 +41,7 @@ export default {
getSearch() {
const searchQuestion = this.$route.query.q;
this.$youtube.search(searchQuestion).then((response) => {
this.videos = response.items;
this.renderer = response;
});
},
},

View File

@ -86,8 +86,8 @@
</v-sheet>
</v-bottom-sheet> -->
</v-card>
<horizontal-list-renderer :recommends="recommends" />
<vid-load-renderer v-if="!recommends" />
<shelf-renderer v-else :render="recommends" />
</div>
</template>
@ -100,10 +100,11 @@
<script>
import { Share } from "@capacitor/share";
import horizontalListRenderer from "../components/horizontalListRenderer.vue";
import ShelfRenderer from "../components/SectionRenderers/shelfRenderer.vue";
import VidLoadRenderer from "../components/vidLoadRenderer.vue";
export default {
components: { horizontalListRenderer },
components: { ShelfRenderer, VidLoadRenderer },
data() {
return {
interactions: [
@ -137,7 +138,7 @@ export default {
vidSrc: null,
description: null,
views: null,
recommends: [],
recommends: null,
loaded: false,
};
},
@ -197,7 +198,7 @@ export default {
this.$vuetube.statusBar.show();
this.$vuetube.navigationBar.show();
}
}
},
},
watch: {
// Watch for change in the route query string (in this case, ?v=xxxxxxxx to ?v=yyyyyyyy)

View File

@ -246,7 +246,7 @@ class Innertube {
)?.slimVideoDescriptionRenderer.description.runs,
recommendations: columnUI?.contents.find(
(contents) => contents.shelfRenderer
).shelfRenderer?.content?.horizontalListRenderer?.items,
).shelfRenderer,
recommendationsContinuation:
columnUI?.continuations[0].reloadContinuationData?.continuation,
},
@ -264,9 +264,7 @@ class Innertube {
`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;
return search.data;
}
}

View File

@ -14,83 +14,6 @@ function logger(func, data, isError = false) {
});
}
//--- Youtube Base Parser ---//
function youtubeParse(html, callback) {
//--- Replace Encoded Characters ---///
html = html.replace(/\\x([0-9A-F]{2})/gi, (...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(constants.LOGGER_NAMES.search, results);
callback(results);
} else {
try {
results = JSON.parse(
html
.split('{"itemSectionRenderer":{"contents":')
[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(
',"continuations":[{'
)[0]
);
logger(constants.LOGGER_NAMES.search, results);
callback(results);
} catch (e) {}
try {
results = JSON.parse(
html
.split('{"itemSectionRenderer":')
[html.split('{"itemSectionRenderer":').length - 1].split(
'},{"continuationItemRenderer":{'
)[0]
).contents;
logger(constants.LOGGER_NAMES.search, results);
callback(results);
} catch (e) {}
}
}
//--- Search Main Function ---//
function youtubeSearch(text, callback) {
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);
});
})
.catch((err) => {
logger(constants.LOGGER_NAMES.search, err, true);
callback(err);
});
}
const searchModule = {
logs: new Array(),
@ -111,66 +34,6 @@ const searchModule = {
});
},
// 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);
// },
getRemainingVideoInfo(id, callback) {
String.prototype.decodeEscapeSequence = function () {
return this.replace(/\\x([0-9A-Fa-f]{2})/g, function () {
return String.fromCharCode(parseInt(arguments[1], 16));
});
};
Http.request({
method: "GET",
url: `${constants.URLS.YT_URL}/watch`,
params: { v: id },
})
.then((res) => {
let dataUpdated = res.data.decodeEscapeSequence();
let likes = dataUpdated
.split(
`"defaultIcon":{"iconType":"LIKE"},"defaultText":{"runs":[{"text":"`
)[1]
.split(`"}],"accessibility":`)[0];
let uploadDate = dataUpdated
.split(`"uploadDate":"`)[1]
.split(`}},"trackingParams":"`)[0]
.slice(0, -2);
let data = {
likes: likes,
uploadDate: uploadDate,
};
logger("vidData", data);
callback(data);
})
.catch((err) => {
logger("codeRun", err, true);
callback(err);
});
},
getReturnYoutubeDislike(id, callback) {
Http.request({
method: "GET",
@ -212,7 +75,7 @@ const innertubeModule = {
}
},
getThumbnail(id, resolution) {
getThumbnail(id, resolution, backupThumbnail) {
if (resolution == "max") {
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
let img = new Image();
@ -221,7 +84,9 @@ const innertubeModule = {
if (img.height !== 120) return url;
};
}
return `https://img.youtube.com/vi/${id}/mqdefault.jpg`;
if (backupThumbnail[backupThumbnail.length - 1])
return backupThumbnail[backupThumbnail.length - 1].url;
else return `https://img.youtube.com/vi/${id}/mqdefault.jpg`;
},
// It just works™
@ -235,8 +100,7 @@ const innertubeModule = {
response.data.contents.singleColumnBrowseResultsRenderer.tabs[0]
.tabRenderer.content.sectionListRenderer.contents;
const final = contents.map((shelves) => {
const video =
shelves.shelfRenderer?.content?.horizontalListRenderer?.items;
const video = shelves.shelfRenderer?.content?.horizontalListRenderer;
if (video) return video;
});
@ -247,7 +111,7 @@ const innertubeModule = {
async search(query) {
try {
const response = await InnertubeAPI.getSearchAsync(query);
return response.content.verticalListRenderer;
return response.contents.sectionListRenderer;
} catch (err) {
logger(constants.LOGGER_NAMES.search, err, true);
}