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"> <div style="position: relative" class="thumbnail-container">
<v-img <v-img
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(video.videoId, 'max')" :src="
$youtube.getThumbnail(
video.videoId,
'max',
video.thumbnail.thumbnails
)
"
/> />
<div <div
class="videoRuntimeFloat" 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"> <div style="position: relative" class="thumbnail-container">
<v-img <v-img
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(video.videoId, 'max')" :src="
$youtube.getThumbnail(
video.videoId,
'max',
video.thumbnail.thumbnails
)
"
/> />
<div <div
class="videoRuntimeFloat" class="videoRuntimeFloat"
@ -131,5 +137,9 @@ export default {
return bottomText.join(" · "); return bottomText.join(" · ");
}, },
}, },
mounted() {
console.log("gridVideoRenderer received: ", this.video);
},
}; };
</script> </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 * This is to allow use of "recommended" videos on other pages such as /watch
* -Front * -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> </template>
<script> <script>
import horizontalListRenderer from "../components/horizontalListRenderer.vue"; import horizontalListRenderer from "../components/ListRenderers/horizontalListRenderer.vue";
import VidLoadRenderer from "../components/vidLoadRenderer.vue";
export default { export default {
components: { horizontalListRenderer }, components: { horizontalListRenderer, VidLoadRenderer },
computed: { computed: {
recommends: { recommends: {

View File

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

View File

@ -86,8 +86,8 @@
</v-sheet> </v-sheet>
</v-bottom-sheet> --> </v-bottom-sheet> -->
</v-card> </v-card>
<vid-load-renderer v-if="!recommends" />
<horizontal-list-renderer :recommends="recommends" /> <shelf-renderer v-else :render="recommends" />
</div> </div>
</template> </template>
@ -100,10 +100,11 @@
<script> <script>
import { Share } from "@capacitor/share"; 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 { export default {
components: { horizontalListRenderer }, components: { ShelfRenderer, VidLoadRenderer },
data() { data() {
return { return {
interactions: [ interactions: [
@ -137,7 +138,7 @@ export default {
vidSrc: null, vidSrc: null,
description: null, description: null,
views: null, views: null,
recommends: [], recommends: null,
loaded: false, loaded: false,
}; };
}, },
@ -197,7 +198,7 @@ export default {
this.$vuetube.statusBar.show(); this.$vuetube.statusBar.show();
this.$vuetube.navigationBar.show(); this.$vuetube.navigationBar.show();
} }
} },
}, },
watch: { watch: {
// Watch for change in the route query string (in this case, ?v=xxxxxxxx to ?v=yyyyyyyy) // 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, )?.slimVideoDescriptionRenderer.description.runs,
recommendations: columnUI?.contents.find( recommendations: columnUI?.contents.find(
(contents) => contents.shelfRenderer (contents) => contents.shelfRenderer
).shelfRenderer?.content?.horizontalListRenderer?.items, ).shelfRenderer,
recommendationsContinuation: recommendationsContinuation:
columnUI?.continuations[0].reloadContinuationData?.continuation, columnUI?.continuations[0].reloadContinuationData?.continuation,
}, },
@ -264,9 +264,7 @@ class Innertube {
`Could not get search results: ${search.status_code} - ${search.message}` `Could not get search results: ${search.status_code} - ${search.message}`
); );
console.log(search.data); console.log(search.data);
return search.data.contents.sectionListRenderer.contents.find( return search.data;
(contents) => contents.shelfRenderer
).shelfRenderer;
} }
} }

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 = { const searchModule = {
logs: new Array(), 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) { getReturnYoutubeDislike(id, callback) {
Http.request({ Http.request({
method: "GET", method: "GET",
@ -212,7 +75,7 @@ const innertubeModule = {
} }
}, },
getThumbnail(id, resolution) { getThumbnail(id, resolution, backupThumbnail) {
if (resolution == "max") { if (resolution == "max") {
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`; const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
let img = new Image(); let img = new Image();
@ -221,7 +84,9 @@ const innertubeModule = {
if (img.height !== 120) return url; 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™ // It just works™
@ -235,8 +100,7 @@ const innertubeModule = {
response.data.contents.singleColumnBrowseResultsRenderer.tabs[0] response.data.contents.singleColumnBrowseResultsRenderer.tabs[0]
.tabRenderer.content.sectionListRenderer.contents; .tabRenderer.content.sectionListRenderer.contents;
const final = contents.map((shelves) => { const final = contents.map((shelves) => {
const video = const video = shelves.shelfRenderer?.content?.horizontalListRenderer;
shelves.shelfRenderer?.content?.horizontalListRenderer?.items;
if (video) return video; if (video) return video;
}); });
@ -247,7 +111,7 @@ const innertubeModule = {
async search(query) { async search(query) {
try { try {
const response = await InnertubeAPI.getSearchAsync(query); const response = await InnertubeAPI.getSearchAsync(query);
return response.content.verticalListRenderer; return response.contents.sectionListRenderer;
} catch (err) { } catch (err) {
logger(constants.LOGGER_NAMES.search, err, true); logger(constants.LOGGER_NAMES.search, err, true);
} }