mirror of https://github.com/VueTubeApp/VueTube
Merge branch 'dev' of github.com-picklenik:Frontesque/VueTube into channel-stuff
This commit is contained in:
commit
199b6041c0
|
@ -2,6 +2,17 @@
|
|||
<v-card
|
||||
class="entry gridVideoRenderer background"
|
||||
:to="`/watch?v=${video.videoId}`"
|
||||
:class="
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
margin: $store.state.tweaks.roundTweak > 0 ? '0.5rem 1rem' : '0',
|
||||
}"
|
||||
flat
|
||||
>
|
||||
<div id="details">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<v-list-item
|
||||
v-for="(video, index) in render.items"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<v-list-item
|
||||
v-for="(renderer, index) in render.contents"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(renderer)[0]]"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<v-list-item
|
||||
v-for="(video, index) in render.items"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
<div class="fill-width">
|
||||
<v-list-item
|
||||
v-for="(video, index) in render.contents"
|
||||
:key="index"
|
||||
class="pa-0 fill-screen"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
|
@ -11,13 +11,13 @@
|
|||
:key="video[Object.keys(video)[0]].videoId"
|
||||
:video="video[Object.keys(video)[0]]"
|
||||
></component>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<div
|
||||
v-if="
|
||||
render.separatorDetails && render.separatorDetails.hasBottomSeparator
|
||||
"
|
||||
class="separator-bottom background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-4' : 'darken-4'"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
:style="{ height: render.separatorDetails.height + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
|
@ -28,10 +28,6 @@
|
|||
width: 100%; /* Prevent Loading Weirdness */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fill-screen {
|
||||
width: 100vw; /* Very Hacky */
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="fill-width">
|
||||
<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>
|
||||
<v-list-item class="pa-0 min-height-0">
|
||||
<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
|
||||
></v-list-item>
|
||||
<div
|
||||
v-if="render.separator && render.separator.hasBottomSeparator"
|
||||
class="separator-bottom background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-4' : 'darken-4'"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
:style="{ height: render.separator.height + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
<template>
|
||||
<v-card class="entry videoRenderer background" :to="`/watch?v=${vidId}`" flat>
|
||||
<div style="position: relative" class="thumbnail-container">
|
||||
<v-card
|
||||
class="entry videoRenderer background"
|
||||
:to="`/watch?v=${vidId}`"
|
||||
:class="
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
margin: $store.state.tweaks.roundTweak > 0 ? '0.5rem 1rem' : '0',
|
||||
}"
|
||||
flat
|
||||
>
|
||||
<div style="position: relative" class="thumbnail-container overflow-hidden">
|
||||
<v-img
|
||||
:aspect-ratio="16 / 9"
|
||||
:src="$youtube.getThumbnail(vidId, 'max', thumbnails)"
|
||||
|
@ -108,6 +123,12 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
vidId: {
|
||||
type: String,
|
||||
|
|
|
@ -56,11 +56,13 @@
|
|||
</v-app>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import { mapState } from "vuex";
|
||||
import constants from "~/plugins/constants";
|
||||
import { linkParser } from "~/plugins/utils";
|
||||
import backType from "~/plugins/classes/backType";
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
|
@ -92,22 +94,7 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
//--- Back Button Listener ---//
|
||||
this.backHandler = CapacitorApp.addListener(
|
||||
"backButton",
|
||||
({ canGoBack }) => {
|
||||
//--- Back Closes Search ---//
|
||||
if (this.search) {
|
||||
this.search = false;
|
||||
|
||||
//--- Back Goes Back ---//
|
||||
} else if (!canGoBack) {
|
||||
CapacitorApp.exitApp();
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
);
|
||||
this.$vuetube.resetBackActions();
|
||||
|
||||
// --- External URL Handling --- //
|
||||
CapacitorApp.addListener("appUrlOpen", (event) => {
|
||||
|
@ -176,6 +163,17 @@ export default {
|
|||
}
|
||||
} else {
|
||||
this.search = true;
|
||||
|
||||
// Adds to the back stack
|
||||
const closeSearch = new backType(
|
||||
() => {
|
||||
this.search = false;
|
||||
},
|
||||
() => {
|
||||
return this.search;
|
||||
}
|
||||
);
|
||||
this.$vuetube.addBackAction(closeSearch);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -235,6 +233,14 @@ div {
|
|||
vertical-align: -0.1em;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.min-height-0 {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.fill-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
computed: {
|
||||
recommends: {
|
||||
get() {
|
||||
return this.$store.state.recommendedVideos;
|
||||
return [...this.$store.state.recommendedVideos];
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("updateRecommendedVideos", val);
|
||||
|
|
|
@ -55,8 +55,11 @@ export default {
|
|||
);
|
||||
|
||||
await theming;
|
||||
this.progressMsg = "Communicating with the API";
|
||||
await this.$youtube.getAPI();
|
||||
this.progressMsg = "Launching";
|
||||
this.progressMsg = "Launching startup tasks";
|
||||
await this.$vuetube.launchBackHandling();
|
||||
this.progressMsg = "Navigating to the home page";
|
||||
|
||||
this.$router.replace(`/${localStorage.getItem("startPage") || "home"}`); // Prevent user from navigating back to the splash screen
|
||||
},
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
}"
|
||||
id="content-container"
|
||||
>
|
||||
<v-card v-if="loaded" class="ml-2 mr-2 background rounded-0" flat>
|
||||
<v-card v-if="loaded" class="px-2 background rounded-0" flat>
|
||||
<div
|
||||
v-ripple
|
||||
class="d-flex justify-space-between align-start px-3 pt-3"
|
||||
|
@ -152,21 +152,18 @@
|
|||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<div
|
||||
v-if="loaded && video.commentData"
|
||||
@click="showComments = !showComments"
|
||||
>
|
||||
<v-card flat class="background comment-renderer">
|
||||
<v-text class="comment-count keep-spaces">
|
||||
<div v-if="loaded && video.commentData" @click="toggleComment">
|
||||
<v-card flat tile class="background comment-renderer py-0">
|
||||
<v-card-text class="comment-count keep-spaces px-0">
|
||||
<template v-for="text in video.commentData.headerText.runs">
|
||||
<template v-if="text.bold">
|
||||
<strong :key="text.text">{{ text.text }}</strong>
|
||||
</template>
|
||||
<template v-else>{{ text.text }}</template>
|
||||
</template>
|
||||
</v-text>
|
||||
<v-icon v-if="showComments">mdi-unfold-less-horizontal</v-icon>
|
||||
<v-icon v-else>mdi-unfold-more-horizontal</v-icon>
|
||||
</v-card-text>
|
||||
<v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon>
|
||||
<v-icon v-else dense>mdi-unfold-more-horizontal</v-icon>
|
||||
</v-card>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
@ -209,13 +206,13 @@ import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
|||
import { getCpn } from "~/plugins/utils";
|
||||
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
|
||||
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
||||
import legacyPlayer from "~/components/Player/legacy.vue"
|
||||
import legacyPlayer from "~/components/Player/legacy.vue";
|
||||
import vuetubePlayer from "~/components/Player/index.vue";
|
||||
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
||||
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
|
||||
import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
|
||||
|
||||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import backType from "~/plugins/classes/backType";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -259,29 +256,11 @@ export default {
|
|||
|
||||
mounted() {
|
||||
this.mountedInit();
|
||||
|
||||
this.backHandler = CapacitorApp.addListener(
|
||||
"backButton",
|
||||
({ canGoBack }) => {
|
||||
//--- Back Closes Search ---//
|
||||
if (this.showComments) {
|
||||
this.showComments = false;
|
||||
|
||||
//--- Back Goes Back ---//
|
||||
} else if (!canGoBack) {
|
||||
this.$router.replace(
|
||||
`/${localStorage.getItem("startPage") || "home"}`
|
||||
);
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
);
|
||||
this.$vuetube.resetBackActions();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
if (this.backHandler) this.backHandler.remove();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -412,6 +391,7 @@ export default {
|
|||
interval: null,
|
||||
video: null,
|
||||
useBetaPlayer: false,
|
||||
backHierarchy: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -426,6 +406,22 @@ export default {
|
|||
scrollable.scrollTo(0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
// Toggle this.showComments to true or false. If it is true, then add the dismiss function to backStack.
|
||||
toggleComment() {
|
||||
this.showComments = !this.showComments;
|
||||
if (this.showComments) {
|
||||
const dismissComment = new backType(
|
||||
() => {
|
||||
this.showComments = false;
|
||||
},
|
||||
() => {
|
||||
return this.showComments;
|
||||
}
|
||||
);
|
||||
this.$vuetube.addBackAction(dismissComment);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import backType from "./backType";
|
||||
export default class backHandler {
|
||||
constructor() {
|
||||
this.backStack = []; // This should only contain instances of backType. Any other type will be ignored.
|
||||
this.startUp();
|
||||
}
|
||||
|
||||
startUp() {
|
||||
this.reset();
|
||||
|
||||
// Add a listener for the back button.
|
||||
this.backHandler = CapacitorApp.addListener("backButton", (context) => { this.back(context) });
|
||||
|
||||
// Start garbage collection. Run every 5 minutes.
|
||||
setInterval(() => {
|
||||
() => { this.garbageCollect };
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.backStack = [];
|
||||
}
|
||||
|
||||
back({ canGoBack }) {
|
||||
console.log("backStack", this.backStack)
|
||||
// Check if backStack contains any backType objects. If so, call the goBack() function.
|
||||
if (this.backStack.length > 0) {
|
||||
// Loop through the backStack array.
|
||||
let lastResult = false;
|
||||
while (!lastResult && this.backStack.length > 0) {
|
||||
const backAction = this.backStack.pop();
|
||||
lastResult = backAction.goBack();
|
||||
}
|
||||
// Since a function was successfully called, no need to continue.
|
||||
if (lastResult) return;
|
||||
}
|
||||
if (!canGoBack) {
|
||||
// If we can't go back, then we should exit the app.
|
||||
CapacitorApp.exitApp();
|
||||
} else {
|
||||
// If we can go back, then we should go back.
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
addAction(callback) {
|
||||
if (callback instanceof backType) {
|
||||
this.backStack.push(callback);
|
||||
} else {
|
||||
throw new TypeError("backType object expected");
|
||||
}
|
||||
}
|
||||
|
||||
// Loops through the backStack array if array larger than 10. If backType.check() returns false, then remove it from the backStack array.
|
||||
garbageCollect() {
|
||||
if (this.backStack.length > 10) {
|
||||
this.backStack = this.backStack.filter((backType) => backType.check());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// This is the class that populates the backStack array in the backHandler class.
|
||||
export default class backType {
|
||||
constructor(callback, check) {
|
||||
this.callback = callback;
|
||||
this.check = check || (() => true);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
if (this.check()) {
|
||||
this.callback();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ module.exports = {
|
|||
recommendations: "Recommendations",
|
||||
init: "Initialize",
|
||||
innertube: "Innertube",
|
||||
channel: "Channel",
|
||||
},
|
||||
|
||||
INNERTUBE_HEADER: (info) => {
|
||||
|
|
|
@ -76,18 +76,23 @@ class Innertube {
|
|||
|
||||
//--- API Calls ---//
|
||||
|
||||
async browseAsync(action_type) {
|
||||
async browseAsync(action_type, args) {
|
||||
let data = { context: this.context };
|
||||
|
||||
switch (action_type) {
|
||||
case "recommendations":
|
||||
data.browseId = "FEwhat_to_watch";
|
||||
args.browseId = "FEwhat_to_watch";
|
||||
break;
|
||||
case "playlist":
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
case "channel":
|
||||
if (args && args.browseId) {
|
||||
break;
|
||||
} else {
|
||||
throw new ReferenceError("No browseId provided");
|
||||
}
|
||||
default:
|
||||
}
|
||||
data.browseId = { ...data, args };
|
||||
|
||||
console.log(data);
|
||||
|
||||
|
@ -217,6 +222,28 @@ class Innertube {
|
|||
};
|
||||
}
|
||||
|
||||
async getEndPoint(url) {
|
||||
let data = { context: this.context, url: url };
|
||||
const response = await Http.post({
|
||||
url: `${constants.URLS.YT_BASE_API}/navigation/resolve_url?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,
|
||||
};
|
||||
}
|
||||
|
||||
// WARNING: This is tracking the user's activity, but is required for recommendations to properly work
|
||||
async apiStats(params, url) {
|
||||
console.log(params);
|
||||
|
@ -252,10 +279,24 @@ class Innertube {
|
|||
// Simple Wrappers
|
||||
async getRecommendationsAsync() {
|
||||
const rec = await this.browseAsync("recommendations");
|
||||
console.log(rec.data);
|
||||
return rec;
|
||||
}
|
||||
|
||||
async getChannelAsync(url) {
|
||||
const channelEndpoint = await this.getEndPoint(url);
|
||||
if (
|
||||
channelEndpoint.success &&
|
||||
channelEndpoint.data.endpoint?.browseEndpoint
|
||||
) {
|
||||
return await this.browseAsync(
|
||||
"channel",
|
||||
channelEndpoint.data.endpoint?.browseEndpoint
|
||||
);
|
||||
} else {
|
||||
throw new ReferenceError("Cannot find channel");
|
||||
}
|
||||
}
|
||||
|
||||
async VidInfoAsync(id) {
|
||||
let response = await this.getVidAsync(id);
|
||||
|
||||
|
@ -298,7 +339,9 @@ class Innertube {
|
|||
isLive: details.isLiveContent,
|
||||
channelName: details.author,
|
||||
channelSubs: ownerData?.collapsedSubtitle?.runs[0]?.text,
|
||||
channelUrl: rendererUtils.getNavigationEndpoints(ownerData),
|
||||
channelUrl: rendererUtils.getNavigationEndpoints(
|
||||
ownerData.navigationEndpoint
|
||||
),
|
||||
channelImg: ownerData?.thumbnail?.thumbnails[0].url,
|
||||
availableResolutions: resolutions?.formats,
|
||||
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
// General utility functions for the renderers
|
||||
class rendererUtils {
|
||||
static getNavigationEndpoints(base) {
|
||||
const navEndpoint = base.navigationEndpoint;
|
||||
if (!navEndpoint) return;
|
||||
if (navEndpoint.urlEndpoint) {
|
||||
const params = new Proxy(
|
||||
new URLSearchParams(navEndpoint.urlEndpoint.url),
|
||||
{
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
}
|
||||
);
|
||||
if (!base) return;
|
||||
if (base.urlEndpoint) {
|
||||
const params = new Proxy(new URLSearchParams(base.urlEndpoint.url), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
if (params.q) return decodeURI(params.q);
|
||||
else return new URL(navEndpoint.urlEndpoint.url).pathname;
|
||||
} else if (navEndpoint.browseEndpoint) {
|
||||
return navEndpoint.browseEndpoint.canonicalBaseUrl;
|
||||
} else if (navEndpoint.watchEndpoint) {
|
||||
return `/watch?v=${navEndpoint.watchEndpoint.videoId}`;
|
||||
} else if (navEndpoint.navigationEndpoint) {
|
||||
else return new URL(base.urlEndpoint.url).pathname;
|
||||
} else if (base.browseEndpoint) {
|
||||
return base.browseEndpoint.canonicalBaseUrl;
|
||||
} else if (base.watchEndpoint) {
|
||||
return `/watch?v=${base.watchEndpoint.videoId}`;
|
||||
} else if (base.navigationEndpoint) {
|
||||
return; //for now
|
||||
} else if (navEndpoint.searchEndpoint) {
|
||||
return `/search?q=${encodeURI(navEndpoint.searchEndpoint.query)}`;
|
||||
} else if (base.searchEndpoint) {
|
||||
return `/search?q=${encodeURI(base.searchEndpoint.query)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import constants from "./constants";
|
|||
import { hexToRgb, rgbToHex, parseEmoji } from "./utils";
|
||||
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
||||
import Vue from "vue";
|
||||
import backHandler from "./classes/backHander";
|
||||
|
||||
Vue.directive("emoji", {
|
||||
inserted: function (el) {
|
||||
|
@ -14,6 +15,8 @@ Vue.directive("emoji", {
|
|||
},
|
||||
});
|
||||
|
||||
let backActions;
|
||||
|
||||
const module = {
|
||||
//--- Get GitHub Commits ---//
|
||||
commits: new Promise((resolve, reject) => {
|
||||
|
@ -109,6 +112,23 @@ const module = {
|
|||
rgbToHex(r, g, b) {
|
||||
return rgbToHex(r, g, b);
|
||||
},
|
||||
|
||||
async launchBackHandling() {
|
||||
backActions = new backHandler();
|
||||
return true;
|
||||
},
|
||||
|
||||
resetBackActions() {
|
||||
backActions.reset();
|
||||
},
|
||||
|
||||
addBackAction(action) {
|
||||
backActions.addAction(action);
|
||||
},
|
||||
|
||||
back(listenerFunc) {
|
||||
backActions.back(listenerFunc);
|
||||
},
|
||||
};
|
||||
|
||||
//--- Start ---//
|
||||
|
|
|
@ -109,6 +109,15 @@ const innertubeModule = {
|
|||
else return `https://img.youtube.com/vi/${id}/mqdefault.jpg`;
|
||||
},
|
||||
|
||||
async getChannel(url) {
|
||||
try {
|
||||
const response = await InnertubeAPI.getChannelAsync(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger(constants.LOGGER_NAMES.channel, error, true);
|
||||
}
|
||||
},
|
||||
|
||||
// It just works™
|
||||
// Front page recommendation
|
||||
async recommend() {
|
||||
|
|
Loading…
Reference in New Issue