mirror of
https://github.com/VueTubeApp/VueTube
synced 2024-11-29 22:53:05 +00:00
feat: new render method that directly utilizes the youtube API
This commit is contained in:
parent
90708b9dac
commit
979691a978
6 changed files with 256 additions and 99 deletions
46
NUXT/components/VideoRenderers/compactVideoRenderer.vue
Normal file
46
NUXT/components/VideoRenderers/compactVideoRenderer.vue
Normal 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>
|
113
NUXT/components/VideoRenderers/gridVideoRenderer.vue
Normal file
113
NUXT/components/VideoRenderers/gridVideoRenderer.vue
Normal 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>
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ---//
|
||||
|
|
Loading…
Reference in a new issue