0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-29 06:33:05 +00:00

Merge pull request #156 from PickleNik/main

great work nik (for once) /s
This commit is contained in:
Kenny 2022-03-21 20:44:14 -04:00 committed by GitHub
commit 15aef74075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1087 additions and 872 deletions

10
NUXT/.eslintignore Normal file
View file

@ -0,0 +1,10 @@
# js vendor file with import/require
assets/vendor/**
# static vendor file . use with nuxt.config.js script
static/**
# dependencies
node_modules
# Nuxt build
.nuxt
# Nuxt generate
dist

24
NUXT/.eslintrc.js Normal file
View file

@ -0,0 +1,24 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
},
parserOptions: {
parser: "babel-eslint",
},
extends: [
"prettier",
"eslint:recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
],
plugins: ["vue"],
rules: {
"vue/multi-word-component-names": 0,
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// 'prettier/prettier': ['error', { semi: false }],
// semi: [2, 'never'],
},
};

View file

@ -6,19 +6,34 @@
# install dependencies
$ npm install
# serve with hot reload at localhost:3000
# serve with hot reload
$ npm run dev
# build for production and launch server
# generate /android and /ios builds
$ npm run build
$ npm run start
# generate static project
$ npm run generate
# lint files to avoid errors and formatting issues (do this before PR)
$ npm run lint
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Recommended VSCode Setup for Auto-Formatting
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) `ext install dbaeumer.vscode-eslint`
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) `ext install esbenp.prettier-vscode`
- [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur)
>`Ctrl(Cmd)` + `Shift` + `P` > Open Settings (JSON)
```json
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
"vetur.validation.template": false,
```
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.

View file

@ -1,18 +1,22 @@
<template>
<v-bottom-navigation v-model="tabSelection" shift class="bottomNav py-4 accent2">
<v-bottom-navigation
v-model="tabSelection"
shift
class="bottomNav py-4 accent2"
>
<v-btn
v-for="(item, i) in tabs"
:key="i"
v-ripple="false"
class="navButton"
:to="item.link"
plain
v-ripple="false"
>
<span v-text="item.name" />
<v-icon
v-text="item.icon"
:color="tabSelection == i ? 'primary' : 'grey'"
:class="tabSelection == i ? 'tab primaryAlt' : ''"
v-text="item.icon"
/>
</v-btn>
<!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()"

View file

@ -12,12 +12,12 @@
<div style="position: relative">
<v-img :src="video.thumbnail" />
<div
v-text="video.metadata.overlay[0]"
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.metadata.overlay[0]"
/>
</div>
<div v-text="video.title" style="margin-top: 0.5em" />
<div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="parseBottom(video)" />
</v-card-text>
</v-card>

View file

@ -4,22 +4,27 @@
color="accent2"
class="topNav rounded-0 pa-3"
>
<h3 class="my-auto ml-4" v-text="page" v-show="!search" />
<h3 v-show="!search" class="my-auto ml-4" v-text="page" />
<v-btn icon v-if="search" class="mr-3 my-auto" @click="$emit('close-search')">
<v-btn
v-if="search"
icon
class="mr-3 my-auto"
@click="$emit('close-search')"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-text-field
v-if="search"
v-model="text"
solo
dense
flat
label="Search"
v-model="text"
@input="$emit('text-changed', text)"
class="searchBar"
v-if="search"
v-on:keyup.enter="$emit('search-btn', text)"
@input="$emit('text-changed', text)"
@keyup.enter="$emit('search-btn', text)"
/>
<v-spacer v-if="!search" />
@ -33,11 +38,11 @@
><v-icon>mdi-magnify</v-icon></v-btn
>
<v-btn
v-show="!search"
icon
tile
class="ml-4 mr-2 my-auto fill-height"
style="border-radius: 0.25rem !important"
v-show="!search"
to="/settings"
><v-icon>mdi-dots-vertical</v-icon></v-btn
>
@ -46,7 +51,16 @@
<script>
export default {
props: ["search", "page"],
props: {
search: {
type: Boolean,
default: false,
},
page: {
type: String,
default: "Home",
},
},
events: ["searchBtn", "textChanged", "closeSearch"],
data: () => ({
text: "",

View file

@ -1,37 +1,47 @@
<template>
<div>
<v-snackbar v-model="updateSnackbar" class="updateBar" :timeout="updateSnackbarTimeout">
{{ updateSnackbarText }}
<div>
<v-snackbar
v-model="updateSnackbar"
class="updateBar"
:timeout="updateSnackbarTimeout"
>
{{ updateSnackbarText }}
<template v-slot:action="{ attrs }">
<v-btn color="primary" text v-bind="attrs" @click="updateSnackbar = false">Close</v-btn>
</template>
</v-snackbar>
</div>
<template #action="{ attrs }">
<v-btn
color="primary"
text
v-bind="attrs"
@click="updateSnackbar = false"
>Close</v-btn
>
</template>
</v-snackbar>
</div>
</template>
<style scoped>
.updateBar {
z-index: 99999999;
z-index: 99999999;
}
</style>
<script>
export default {
data() {
return {
updateSnackbar: false,
updateSnackbarText: "An update is available",
updateSnackbarTimeout: 5000
}
},
data() {
return {
updateSnackbar: false,
updateSnackbarText: "An update is available",
updateSnackbarTimeout: 5000,
};
},
async mounted() {
const commits = await this.$vuetube.commits;
const appVersion = process.env.appVersion;
if (appVersion !== commits[0].sha && appVersion !== 'dev-local') {
this.updateSnackbar = true;
}
},
}
async mounted() {
const commits = await this.$vuetube.commits;
const appVersion = process.env.appVersion;
if (appVersion !== commits[0].sha && appVersion !== "dev-local") {
this.updateSnackbar = true;
}
},
};
</script>

View file

@ -1,11 +1,11 @@
<template>
<v-app v-show="stateLoaded" style="background: black !important">
<topNavigation
:search="search"
:page="page"
@close-search="search = !search"
@search-btn="searchBtn"
@text-changed="textChanged"
:search="search"
:page="page"
/>
<div class="accent2" style="height: 100%; margin-top: 4rem">
@ -41,7 +41,7 @@
"
>
<div class="scroll-y" style="height: 100%">
<div style="min-width: 180px" v-if="search">
<div v-if="search" style="min-width: 180px">
<v-list-item v-for="(item, index) in response" :key="index">
<v-icon>mdi-magnify</v-icon>
<v-btn
@ -65,8 +65,8 @@
<style>
* {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.scroll-y {
overflow-y: scroll !important; /* has to be scroll, not auto */

View file

@ -1,6 +1,6 @@
<template>
<v-app>
<nuxt />
<nuxt />
</v-app>
</template>

View file

@ -1,31 +1,28 @@
<template>
<v-app>
<center>
<v-icon size="100">mdi-alert-circle</v-icon>
<h1 class="grey--text">An error occured!</h1>
<v-btn to="/">Reload Application</v-btn>
<v-btn to="/logs">Show Logs</v-btn>
<div style="margin-top: 5em; color: #999; font-size: 0.75em;">
<div style="font-size: 1.4em;">Error Information</div>
<div style="margin-top: 5em; color: #999; font-size: 0.75em">
<div style="font-size: 1.4em">Error Information</div>
<div>Code: {{ error.statusCode }}</div>
<div>Path: {{ this.$route.fullPath }}</div>
<div>Path: {{ $route.fullPath }}</div>
</div>
</center>
</v-app>
</template>
<script>
export default {
layout: 'empty',
layout: "empty",
props: {
error: {
type: Object,
default: null
}
}
}
default: null,
},
},
};
</script>

View file

@ -1,59 +1,54 @@
import colors from 'vuetify/es5/util/colors'
import colors from "vuetify/es5/util/colors";
/**** Front's Notes / Don't Remove ****
* Data Storage:
* localStorage.setItem("key", data)
* localStorage.getItem('key')
*/
* Data Storage:
* localStorage.setItem("key", data)
* localStorage.getItem('key')
*/
export default {
//--- Bettertube Stuff ---//
env: {
appVersion: "dev-local",
},
target: 'static',
target: "static",
plugins: [
{ src: "~/plugins/youtube", mode: "client" },
{ src: "~/plugins/vuetube", mode: "client" },
{ src: "~/plugins/ryd", mode: "client"}
{ src: "~/plugins/ryd", mode: "client" },
],
generate: {
dir: '../dist'
dir: "../dist",
},
//--- Bettertube Debugging ---//
server: {
port: 80, // default: 3000 (Note: Running on ports below 1024 requires root privileges!)
host: '0.0.0.0', // default: localhost,
timing: false
host: "0.0.0.0", // default: localhost,
timing: false,
},
//--- Default NUXT Stuff ---//
head: {
title: 'VueTube',
title: "VueTube",
htmlAttrs: {
lang: 'en'
lang: "en",
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'format-detection', content: 'telephone=no' }
]
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ name: "format-detection", content: "telephone=no" },
],
},
css: [],
components: true,
buildModules: [
'@nuxtjs/vuetify',
],
buildModules: ["@nuxtjs/vuetify"],
modules: [],
vuetify: {
customVariables: ['~/assets/variables.scss'],
customVariables: ["~/assets/variables.scss"],
theme: {
dark: false,
options: { customProperties: true },
@ -64,7 +59,7 @@ export default {
accent: "#CD201F",
accent2: "#fff",
background: "#fff",
info: "#000"
info: "#000",
},
dark: {
primary: colors.red.darken2, //colors.blue.darken2
@ -73,8 +68,8 @@ export default {
accent2: "#222",
background: "#333",
info: "#fff",
}
}
}
}
}
},
},
},
},
};

View file

@ -6,7 +6,8 @@
"dev": "nuxt",
"start": "nuxt generate",
"build": "nuxt generate",
"generate": "nuxt generate"
"generate": "nuxt generate",
"lint": "eslint --fix --ext .js,.vue --ignore-path .eslintignore ."
},
"dependencies": {
"@capacitor/splash-screen": "^1.2.2",
@ -20,6 +21,13 @@
"webpack": "^4.46.0"
},
"devDependencies": {
"@nuxtjs/vuetify": "^1.12.3"
"@nuxtjs/vuetify": "^1.12.3",
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.5.1"
}
}

View file

@ -1,6 +1,10 @@
<template>
<center>
<v-img contain style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" />
<v-img
contain
style="margin-top: 5em; max-width: 80%; max-height: 15em"
src="/dev.svg"
/>
<h1 class="grey--text">Page Under Construction</h1>
<p class="grey--text">Please read the VueTube FAQ for more information.</p>
</center>

View file

@ -1,22 +1,25 @@
<template>
<center style="padding: 1em;">
<center style="padding: 1em">
<div class="row pa-4" style="flex-direction: column">
<div>
<v-img src="/icon.svg" width="100px"/>
<v-img src="/icon.svg" width="100px" />
</div>
<v-spacer/>
<v-spacer />
<div>
<h1 class="pageTitle mb-3">VueTube</h1>
<v-btn @click="openExternal('https://github.com/Frontesque/VueTube')"><v-icon>mdi-github</v-icon></v-btn>
<v-btn @click="openExternal('https://discord.gg/7P8KJrdd5W')"><v-icon>mdi-discord</v-icon></v-btn>
<v-btn @click="openExternal('https://github.com/Frontesque/VueTube')"
><v-icon>mdi-github</v-icon></v-btn
>
<v-btn @click="openExternal('https://discord.gg/7P8KJrdd5W')"
><v-icon>mdi-discord</v-icon></v-btn
>
</div>
</div>
<h3 style="margin-top: 2em;">App Information</h3>
<h3 style="margin-top: 2em">App Information</h3>
<div>App Version: {{ version.substring(0, 7) }}</div>
<h3 style="margin-top: 1em;">Device Information</h3>
<h3 style="margin-top: 1em">Device Information</h3>
<div>Platform: {{ deviceInfo.platform }}</div>
<div>OS: {{ deviceInfo.operatingSystem }} ({{ deviceInfo.osVersion }})</div>
<div>Model: {{ deviceInfo.model }}</div>
@ -32,25 +35,25 @@
</style>
<script>
import { Browser } from '@capacitor/browser';
import { Device } from '@capacitor/device';
import { Browser } from "@capacitor/browser";
import { Device } from "@capacitor/device";
export default {
data() {
return {
version: process.env.appVersion,
deviceInfo: "",
}
};
},
async mounted() {
const info = await Device.getInfo();
this.deviceInfo = info;
},
methods: {
async openExternal(url) {
await Browser.open({ url: url });
}
},
},
async mounted () {
const info = await Device.getInfo();
this.deviceInfo = info
}
}
};
</script>

View file

@ -1,24 +1,26 @@
<template>
<div>
<v-list-item v-for="(item, index) in logs" :key="index">
<v-card class="card">
<v-card-title>
<v-chip outlined class="errorChip" color="error" v-if="item.error">Error</v-chip>
<v-chip v-if="item.error" outlined class="errorChip" color="error"
>Error</v-chip
>
{{ item.name }}
<span v-text="`• ${new Date(item.time).toLocaleString()}`" class="date" />
<span
class="date"
v-text="`• ${new Date(item.time).toLocaleString()}`"
/>
</v-card-title>
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-header>More</v-expansion-panel-header>
<v-expansion-panel-content v-text="item.data" class="logContent" />
<v-expansion-panel-content class="logContent" v-text="item.data" />
</v-expansion-panel>
</v-expansion-panels>
</v-card>
</v-list-item>
</div>
</template>
@ -46,12 +48,12 @@
export default {
data() {
return {
logs: new Array()
}
logs: new Array(),
};
},
mounted() {
const logs = this.$youtube.logs
const logs = this.$youtube.logs;
this.logs = logs;
}
}
},
};
</script>

View file

@ -1,50 +1,38 @@
<template>
<div class="mainContainer pt-1">
<v-card class="pb-5">
<v-card-title>Default Page</v-card-title>
<v-card-text>
<v-select
:items="pages"
v-model="page"
:items="pages"
label="Default Page"
solo
></v-select>
</v-card-text>
</v-card>
</div>
</template>
<script>
export default {
data() {
return {
page: "home",
pages: [
"home",
"subscriptions",
"library"
]
}
pages: ["home", "subscriptions", "library"],
};
},
watch: {
page: function (newVal) {
localStorage.setItem("startPage", newVal);
},
},
mounted() {
this.page = localStorage.getItem("startPage") || "home";
},
watch: {
page: function (val, oldVal) {
localStorage.setItem("startPage", val);
}
}
}
};
</script>
<style scoped>

View file

@ -1,6 +1,5 @@
<template>
<div class="py-1">
<v-card class="pb-5">
<v-card-title>Global Base Color</v-card-title>
<v-row class="ml-3 mr-6">
@ -14,13 +13,25 @@
@click="saveTheme($vuetify.theme.dark)"
/>
</section>
<v-btn v-if="$vuetify.theme.dark" text tile class="white--text black" @click="amoled" >
<v-btn
v-if="$vuetify.theme.dark"
text
tile
class="white--text black"
@click="amoled"
>
{{
this.$vuetify.theme.themes.dark.background === '#000'
? 'LCD'
: 'OLED'
$vuetify.theme.themes.dark.background === "#000" ? "LCD" : "OLED"
}}
<v-icon :size="this.$vuetify.theme.themes.dark.background === '#000' ? '.5rem' : '.9rem'" class="ml-2">mdi-brightness-2</v-icon>
<v-icon
:size="
$vuetify.theme.themes.dark.background === '#000'
? '.5rem'
: '.9rem'
"
class="ml-2"
>mdi-brightness-2</v-icon
>
</v-btn>
</v-row>
</v-card>
@ -28,64 +39,35 @@
<v-card class="pb-5">
<v-card-title>Accent Color</v-card-title>
<v-card-text>
<v-alert color="primary" dense outlined type="warning">NOTE: This doesn't save after closing the app (yet)</v-alert>
<v-color-picker dot-size="5" hide-mode-switch mode="hexa" v-model="accentColor" />
<v-alert color="primary" dense outlined type="warning"
>NOTE: This doesn't save after closing the app (yet)</v-alert
>
<v-color-picker
v-model="accentColor"
dot-size="5"
hide-mode-switch
mode="hexa"
/>
</v-card-text>
</v-card>
</div>
</template>
<script>
export default {
data() {
return {
accentColor: '#ffffff'
}
},
mounted() {
this.accentColor = this.$vuetify.theme.themes.dark.primary;
this.roblox = localStorage.getItem('roblox') === 'true';
},
methods: {
amoled() {
this.$vuetify.theme.themes.dark.background === '#000' ? (
this.$vuetify.theme.themes.dark.accent = '#222',
this.$vuetify.theme.themes.dark.accent2 = '#222',
this.$vuetify.theme.themes.dark.background = '#333',
localStorage.setItem("isOled", false)
) : (
this.$vuetify.theme.themes.dark.accent = '#000',
this.$vuetify.theme.themes.dark.accent2 = '#000',
this.$vuetify.theme.themes.dark.background = '#000',
localStorage.setItem("isOled", true)
)
// doesn't work 😭
// console.log(document.getElementsByClassName('v-application--wrap')[0])
// console.log(document.getElementsByClassName('v-application--wrap')[0].style)
// document.getElementsByClassName('v-application--wrap')[0].style.backgroundColor = "#000000 !important"
},
saveTheme(isDark) {
this.$vuetube.statusBar.setBackground(this.$vuetify.theme.currentTheme.accent)
localStorage.setItem("darkTheme", isDark);
},
accentColor: "#ffffff",
};
},
watch: {
accentColor: function (val, oldVal) {
accentColor: function (val) {
this.$vuetify.theme.currentTheme.primary = val;
let primaryAlt = this.$vuetube.hexToRgb(val);
let rgbEdit = 130; //Light Mode
if (localStorage.getItem('darkTheme') === "true") rgbEdit = -80; //Dark Mode
if (localStorage.getItem("darkTheme") === "true") rgbEdit = -80; //Dark Mode
for (const i in primaryAlt) {
primaryAlt[i] = primaryAlt[i] + rgbEdit; //Amount To Lighten By
@ -93,15 +75,45 @@ export default {
if (primaryAlt[i] < 0) primaryAlt[i] = 0;
}
primaryAlt = this.$vuetube.rgbToHex(primaryAlt.r, primaryAlt.g, primaryAlt.b);
primaryAlt = this.$vuetube.rgbToHex(
primaryAlt.r,
primaryAlt.g,
primaryAlt.b
);
this.$vuetify.theme.currentTheme.primaryAlt = primaryAlt;
}
}
},
},
}
mounted() {
this.accentColor = this.$vuetify.theme.themes.dark.primary;
this.roblox = localStorage.getItem("roblox") === "true";
},
methods: {
amoled() {
this.$vuetify.theme.themes.dark.background === "#000"
? ((this.$vuetify.theme.themes.dark.accent = "#222"),
(this.$vuetify.theme.themes.dark.accent2 = "#222"),
(this.$vuetify.theme.themes.dark.background = "#333"),
localStorage.setItem("isOled", false))
: ((this.$vuetify.theme.themes.dark.accent = "#000"),
(this.$vuetify.theme.themes.dark.accent2 = "#000"),
(this.$vuetify.theme.themes.dark.background = "#000"),
localStorage.setItem("isOled", true));
// doesn't work 😭
// console.log(document.getElementsByClassName('v-application--wrap')[0])
// console.log(document.getElementsByClassName('v-application--wrap')[0].style)
// document.getElementsByClassName('v-application--wrap')[0].style.backgroundColor = "#000000 !important"
},
saveTheme(isDark) {
this.$vuetube.statusBar.setBackground(
this.$vuetify.theme.currentTheme.accent
);
localStorage.setItem("darkTheme", isDark);
},
},
};
</script>
<style scoped>

View file

@ -18,14 +18,14 @@
thumb-size="64"
></v-slider>
<v-slider
v-model="roundTweak"
class="mr-2"
label="Inner"
v-model="roundTweak"
:max="4"
step="1"
thumb-size="64"
>
<template v-slot:thumb-label="{ value }">
<template #thumb-label="{ value }">
<div
class="pa-4 white text-red red-text red--text"
:style="{ borderRadius: value * 3 + 'px !important' }"

View file

@ -1,30 +1,42 @@
<template>
<div class="py-2">
<v-list-item v-for="(item, index) in commits" :key="index" class="my-1">
<v-card class="card my-2">
<v-card-title style="padding: 0 0.25em 0 0.75em;">
<v-card-title style="padding: 0 0.25em 0 0.75em">
{{ item.author ? item.author.login : item.commit.author.name }}
<span v-text="`• ${item.sha.substring(0, 7)}`" class="subtitle" />
<span class="subtitle" v-text="`• ${item.sha.substring(0, 7)}`" />
<v-spacer />
<v-chip outlined class="tags" color="orange" v-if="index == 0">Latest</v-chip>
<v-chip outlined class="tags" color="green" v-if="item.sha == installedVersion">Installed</v-chip>
<v-chip v-if="index == 0" outlined class="tags" color="orange"
>Latest</v-chip
>
<v-chip
v-if="item.sha == installedVersion"
outlined
class="tags"
color="green"
>Installed</v-chip
>
</v-card-title>
<div style="margin-left: 1em;">
<div class="date" v-text="new Date(item.commit.committer.date).toLocaleString()" />
<div style="margin-left: 1em">
<div
class="date"
v-text="new Date(item.commit.committer.date).toLocaleString()"
/>
{{ item.commit.message }}
</div>
<v-card-actions>
<v-spacer />
<v-btn @click="openExternal(item)"><v-icon class="btn-icon">mdi-github</v-icon>View</v-btn>
<v-btn disabled @click="install(item)"><v-icon class="btn-icon">mdi-download</v-icon>Install</v-btn>
<v-btn @click="openExternal(item)"
><v-icon class="btn-icon">mdi-github</v-icon>View</v-btn
>
<v-btn disabled @click="install(item)"
><v-icon class="btn-icon">mdi-download</v-icon>Install</v-btn
>
</v-card-actions>
</v-card>
</v-list-item>
</div>
</template>
@ -51,21 +63,22 @@
</style>
<script>
import { Browser } from '@capacitor/browser';
import { Browser } from "@capacitor/browser";
export default {
data() {
return {
commits: new Array(),
installedVersion: process.env.appVersion
}
installedVersion: process.env.appVersion,
};
},
async mounted() {
const commits = await this.$vuetube.commits;
if (commits[0].sha) { //If Commit Valid
if (commits[0].sha) {
//If Commit Valid
this.commits = commits;
} else {
console.log(commits)
console.log(commits);
}
},
methods: {
@ -75,10 +88,9 @@ export default {
install(item) {
this.$vuetube.getRuns(item, (data) => {
console.log(data)
console.log(data);
});
}
}
}
},
},
};
</script>

View file

@ -10,9 +10,13 @@
<v-card-text>
<div style="position: relative">
<v-img :src="video.thumbnails[video.thumbnails.length - 1].url" />
<div v-text="video.runtime" class="videoRuntimeFloat" style="color: #fff" />
<div
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.runtime"
/>
</div>
<div v-text="video.title" style="margin-top: 0.5em" />
<div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="`${video.views} • ${video.uploaded}`" />
</v-card-text>
</v-card>

View file

@ -1,14 +1,17 @@
<template>
<div style="padding-top: 1em;">
<div style="padding-top: 1em">
<v-list-item v-for="(item, index) in settingsItems" :key="index">
<v-btn text class="entry text-left text-capitalize" :to="item.to" :disabled="item.disabled">
<v-icon v-text="item.icon" size="30px" class="icon" />
<v-btn
text
class="entry text-left text-capitalize"
:to="item.to"
:disabled="item.disabled"
>
<v-icon size="30px" class="icon" v-text="item.icon" />
{{ item.name }}
</v-btn>
</v-list-item>
</div>
</div>
</template>
<style scoped>
@ -30,15 +33,28 @@ export default {
settingsItems: [
{ name: "General", icon: "mdi-cog", to: "", disabled: true },
{ name: "Theme", icon: "mdi-brush-variant", to: "/mods/theme" },
{ name: "Player", icon: "mdi-motion-play-outline", to: "", disabled: true },
{ name: "UI Tweaker", icon: "mdi-television-guide", to: "/mods/tweaks" },
{
name: "Player",
icon: "mdi-motion-play-outline",
to: "",
disabled: true,
},
{
name: "UI Tweaker",
icon: "mdi-television-guide",
to: "/mods/tweaks",
},
{ name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" },
{ name: "Plugins", icon: "mdi-puzzle", to: "", disabled: true},
{ name: "Updates", icon: "mdi-cloud-download-outline", to: "/mods/updates" },
{ name: "Plugins", icon: "mdi-puzzle", to: "", disabled: true },
{
name: "Updates",
icon: "mdi-cloud-download-outline",
to: "/mods/updates",
},
{ name: "Logs", icon: "mdi-text-box-outline", to: "/mods/logs" },
{ name: "About", icon: "mdi-information-outline", to: "/mods/about" },
]
}
}
}
],
};
},
};
</script>

View file

@ -1,6 +1,10 @@
<template>
<center>
<v-img contain style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" />
<v-img
contain
style="margin-top: 5em; max-width: 80%; max-height: 15em"
src="/dev.svg"
/>
<h1 class="grey--text">Page Under Construction</h1>
<p class="grey--text">Please read the VueTube FAQ for more information.</p>
</center>

View file

@ -18,11 +18,11 @@
>
<v-btn
text
@click="item.action"
class="vertical-button"
style="padding: 0; margin: 0"
elevation="0"
:disabled="item.disabled"
@click="item.action"
>
<v-icon v-text="item.icon" />
<div v-text="item.value || item.name" />
@ -40,7 +40,7 @@
<p>Channel Stuff</p>
<hr />
</v-card-text>
<div class="scroll-y ml-2 mr-2" v-if="showMore">
<div v-if="showMore" class="scroll-y ml-2 mr-2">
{{ description }}
</div>
@ -72,7 +72,6 @@
</div>
</template>
<style>
.vertical-button span.v-btn__content {
flex-direction: column;
@ -82,12 +81,6 @@
<script>
export default {
methods: {
dislike() {},
share() {
this.share = !this.share;
},
},
data() {
return {
interactions: [
@ -151,5 +144,11 @@ export default {
this.interactions[1].value = data.dislikes.toLocaleString();
});
},
methods: {
dislike() {},
share() {
this.share = !this.share;
},
},
};
</script>

View file

@ -1,67 +1,67 @@
// To centralize certain values and URLs as for easier debugging and refactoring
const url = {
YT_URL: 'https://www.youtube.com',
YT_MOBILE: "https://m.youtube.com",
YT_MUSIC_URL: 'https://music.youtube.com',
YT_BASE_API: 'https://www.youtube.com/youtubei/v1',
YT_SUGGESTIONS: "https://suggestqueries.google.com/complete",
VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube",
}
YT_URL: "https://www.youtube.com",
YT_MOBILE: "https://m.youtube.com",
YT_MUSIC_URL: "https://music.youtube.com",
YT_BASE_API: "https://www.youtube.com/youtubei/v1",
YT_SUGGESTIONS: "https://suggestqueries.google.com/complete",
VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube",
};
const ytApiVal = {
VERSION: "16.25",
CLIENTNAME: "ANDROID",
VERSION_WEB: "2.20220318.00.00",
CLIENT_WEB: 2
}
VERSION: "16.25",
CLIENTNAME: "ANDROID",
VERSION_WEB: "2.20220318.00.00",
CLIENT_WEB: 2,
};
module.exports = {
URLS: url,
YT_API_VALUES: ytApiVal,
URLS: url,
YT_API_VALUES: ytApiVal,
LOGGER_NAMES: {
search: "Search",
autoComplete: "AutoComplete",
watch: "Watch",
recommendations: "Recommendations",
init: "Initialize",
innertube: "Innertube"
},
LOGGER_NAMES: {
search: "Search",
autoComplete: "AutoComplete",
watch: "Watch",
recommendations: "Recommendations",
init: "Initialize",
innertube: "Innertube",
},
INNERTUBE_HEADER: (info) => {
let headers = {
accept: '*/*',
'user-agent': info.userAgent,
'accept-language': `${info.hl}-${info.gl},${info.hl};q=0.9`,
'content-type': 'application/json',
'x-goog-authuser': 0,
'x-goog-visitor-id': info.visitorData || "",
'x-youtube-client-name': ytApiVal.CLIENTNAME,
'x-youtube-client-version': ytApiVal.VERSION,
'x-origin': info.originalUrl,
'origin': info.originalUrl,
};
return headers
},
INNERTUBE_HEADER: (info) => {
let headers = {
accept: "*/*",
"user-agent": info.userAgent,
"accept-language": `${info.hl}-${info.gl},${info.hl};q=0.9`,
"content-type": "application/json",
"x-goog-authuser": 0,
"x-goog-visitor-id": info.visitorData || "",
"x-youtube-client-name": ytApiVal.CLIENTNAME,
"x-youtube-client-version": ytApiVal.VERSION,
"x-origin": info.originalUrl,
origin: info.originalUrl,
};
return headers;
},
INNERTUBE_CLIENT: (info) => {
let client = {
"gl": info.gl,
"hl": info.hl,
"deviceMake": info.deviceMake,
"deviceModel": info.deviceModel,
"userAgent": info.userAgent,
"clientName": ytApiVal.CLIENTNAME,
"clientVersion": ytApiVal.VERSION,
"osName": info.osName,
"osVersion": info.osVersion,
"platform": "MOBILE",
"originalUrl": info.originalUrl,
"configInfo": info.configInfo,
"remoteHost": info.remoteHost,
"visitorData": info.visitorData,
};
return client
},
}
INNERTUBE_CLIENT: (info) => {
let client = {
gl: info.gl,
hl: info.hl,
deviceMake: info.deviceMake,
deviceModel: info.deviceModel,
userAgent: info.userAgent,
clientName: ytApiVal.CLIENTNAME,
clientVersion: ytApiVal.VERSION,
osName: info.osName,
osVersion: info.osVersion,
platform: "MOBILE",
originalUrl: info.originalUrl,
configInfo: info.configInfo,
remoteHost: info.remoteHost,
visitorData: info.visitorData,
};
return client;
},
};

View file

@ -1,180 +1,217 @@
// Code specific to working with the innertube API
// https://www.youtube.com/youtubei/v1
import { Http } from '@capacitor-community/http';
import { getBetweenStrings } from './utils';
import constants from './constants';
import { Http } from "@capacitor-community/http";
import { getBetweenStrings } from "./utils";
import constants from "./constants";
class Innertube {
//--- Initiation ---//
//--- Initiation ---//
constructor(ErrorCallback) {
this.ErrorCallback = ErrorCallback || undefined;
this.retry_count = 0;
}
constructor(ErrorCallback) {
this.ErrorCallback = ErrorCallback || undefined;
this.retry_count = 0
checkErrorCallback() {
return typeof this.ErrorCallback === "function";
}
async initAsync() {
const html = await Http.get({
url: constants.URLS.YT_URL,
params: { hl: "en" },
}).catch((error) => error);
try {
if (html instanceof Error && this.checkErrorCallback)
this.ErrorCallback(html.message, true);
try {
const data = JSON.parse(
getBetweenStrings(html.data, "ytcfg.set(", ");")
);
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.context = data.INNERTUBE_CONTEXT;
this.logged_in = data.LOGGED_IN;
this.context.client = constants.INNERTUBE_CLIENT(this.context.client);
this.header = constants.INNERTUBE_HEADER(this.context.client);
}
} catch (err) {
console.log(err);
if (this.checkErrorCallback) this.ErrorCallback(err, true);
if (this.retry_count >= 10) {
this.initAsync();
} else {
if (this.checkErrorCallback)
this.ErrorCallback("Failed to retrieve Innertube session", true);
}
}
} catch (error) {
this.ErrorCallback(error, true);
}
}
static async createAsync(ErrorCallback) {
const created = new Innertube(ErrorCallback);
await created.initAsync();
return created;
}
//--- API Calls ---//
async browseAsync(action_type) {
let data = { context: this.context };
switch (action_type) {
case "recommendations":
data.browseId = "FEwhat_to_watch";
break;
case "playlist":
data.browseId = args.browse_id;
break;
default:
}
checkErrorCallback() {
return typeof this.ErrorCallback === "function"
}
console.log(data);
async initAsync() {
const html = await Http.get({ url: constants.URLS.YT_URL, params: { hl: "en" } }).catch((error) => error);
try {
if (html instanceof Error && this.checkErrorCallback) this.ErrorCallback(html.message, true);
try {
const data = JSON.parse(getBetweenStrings(html.data, 'ytcfg.set(', ');'));
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.context = data.INNERTUBE_CONTEXT;
this.logged_in = data.LOGGED_IN;
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`,
data: data,
headers: { "Content-Type": "application/json" },
}).catch((error) => error);
this.context.client = constants.INNERTUBE_CLIENT(this.context.client)
this.header = constants.INNERTUBE_HEADER(this.context.client)
}
if (response instanceof Error)
return {
success: false,
status_code: response.status,
message: response.message,
};
} catch (err) {
console.log(err)
if (this.checkErrorCallback) this.ErrorCallback(err, true)
if (this.retry_count >= 10) { this.initAsync() } else { if (this.checkErrorCallback) this.ErrorCallback("Failed to retrieve Innertube session", true); }
}
} catch (error) {
this.ErrorCallback(error, true)
};
return {
success: true,
status_code: response.status,
data: response.data,
};
}
static async createAsync(ErrorCallback) {
const created = new Innertube(ErrorCallback);
await created.initAsync();
return created;
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`;
}
//--- API Calls ---//
async getVidAsync(id) {
let data = { context: this.context, videoId: id };
const response = await Http.get({
url: `https://m.youtube.com/watch?v=${id}&pbj=1`,
params: {},
headers: Object.assign(this.header, {
referer: `https://m.youtube.com/watch?v=${id}`,
"x-youtube-client-name": constants.YT_API_VALUES.CLIENT_WEB,
"x-youtube-client-version": constants.YT_API_VALUES.VERSION_WEB,
}),
}).catch((error) => error);
async browseAsync(action_type) {
let data = { context: this.context }
const responseMobile = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context),
}).catch((error) => error);
switch (action_type) {
case 'recommendations':
data.browseId = 'FEwhat_to_watch'
break;
case 'playlist':
data.browseId = args.browse_id
break;
default:
}
if (response instanceof Error)
return {
success: false,
status_code: response.response.status,
message: response.message,
};
console.log(data)
return {
success: true,
status_code: response.status,
data: { webOutput: response.data, appOutput: responseMobile.data },
};
}
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`,
data: data,
headers: { "Content-Type": "application/json" }
}).catch((error) => error);
// Simple Wrappers
async getRecommendationsAsync() {
const rec = await this.browseAsync("recommendations");
console.log(rec.data);
return rec;
}
if (response instanceof Error) return { success: false, status_code: response.status, message: response.message };
async VidInfoAsync(id) {
let response = await this.getVidAsync(id);
return {
success: true,
status_code: response.status,
data: response.data
};
}
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 response = await Http.get({
url: `https://m.youtube.com/watch?v=${id}&pbj=1`,
params: {},
headers: Object.assign(this.header, {
referer: `https://m.youtube.com/watch?v=${id}`,
'x-youtube-client-name': constants.YT_API_VALUES.CLIENT_WEB,
'x-youtube-client-version': constants.YT_API_VALUES.VERSION_WEB})
}).catch((error) => error);
const responseMobile = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context)
}).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message };
return {
success: true,
status_code: response.status,
data: {webOutput: response.data, appOutput: responseMobile.data}
};
}
// Simple Wrappers
async getRecommendationsAsync() {
const rec = await this.browseAsync("recommendations");
console.log(rec.data)
return rec;
}
async VidInfoAsync(id) {
let response = await this.getVidAsync(id)
if (response.success && (response.data.webOutput[2].playerResponse?.playabilityStatus?.status == ("ERROR" || undefined)))
throw new Error(`Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`)
const responseWeb = response.data.webOutput
const responseApp = response.data.appOutput
const details = responseWeb[2].playerResponse?.videoDetails
const microformat = responseWeb[2].playerResponse?.microformat?.playerMicroformatRenderer
const renderedPanels = responseWeb[3].response?.engagementPanels
const columnUI = responseWeb[3].response?.contents.singleColumnWatchNextResults?.results?.results
const resolutions = responseApp.streamingData
console.log((columnUI.contents).length)
return {
id: details.videoId,
title: details.title || microformat.title?.runs[0].text,
isLive: details.isLiveContent || microformat.liveBroadcastDetails?.isLiveNow || false,
channelName: details.author || microformat.ownerChannelName,
channelUrl: microformat.ownerProfileUrl,
availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: {
description: microformat.description?.runs[0].text,
descriptionShort: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails || microformat.thumbnails?.thumbnails,
isFamilySafe: microformat.isFamilySafe,
availableCountries: microformat.availableCountries,
liveBroadcastDetails: microformat.liveBroadcastDetails,
uploadDate: microformat.uploadDate,
publishDate: microformat.publishDate,
isPrivate: details.isPrivate,
viewCount: details.viewCount || microformat.viewCount,
lengthSeconds: details.lengthSeconds || microformat.lengthSeconds,
likes: parseInt(columnUI?.contents[1]
.slimVideoMetadataSectionRenderer?.contents[1].slimVideoActionBarRenderer?.buttons[0]
.slimMetadataToggleButtonRenderer?.button?.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(/\D/g, '')) // Yes. I know.
},
renderedData: {
description: renderedPanels[0].engagementPanelSectionListRenderer?.content.structuredDescriptionContentRenderer?.items[1].expandableVideoDescriptionBodyRenderer?.descriptionBodyText.runs,
recommendations: columnUI?.contents[(columnUI.contents).length -1].itemSectionRenderer?.contents
}
}
}
if (
response.success &&
response.data.webOutput[2].playerResponse?.playabilityStatus?.status ==
("ERROR" || undefined)
)
throw new Error(
`Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`
);
const responseWeb = response.data.webOutput;
const responseApp = response.data.appOutput;
const details = responseWeb[2].playerResponse?.videoDetails;
const microformat =
responseWeb[2].playerResponse?.microformat?.playerMicroformatRenderer;
const renderedPanels = responseWeb[3].response?.engagementPanels;
const columnUI =
responseWeb[3].response?.contents.singleColumnWatchNextResults?.results
?.results;
const resolutions = responseApp.streamingData;
console.log(columnUI.contents.length);
return {
id: details.videoId,
title: details.title || microformat.title?.runs[0].text,
isLive:
details.isLiveContent ||
microformat.liveBroadcastDetails?.isLiveNow ||
false,
channelName: details.author || microformat.ownerChannelName,
channelUrl: microformat.ownerProfileUrl,
availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: {
description: microformat.description?.runs[0].text,
descriptionShort: details.shortDescription,
thumbnails:
details.thumbnails?.thumbnails || microformat.thumbnails?.thumbnails,
isFamilySafe: microformat.isFamilySafe,
availableCountries: microformat.availableCountries,
liveBroadcastDetails: microformat.liveBroadcastDetails,
uploadDate: microformat.uploadDate,
publishDate: microformat.publishDate,
isPrivate: details.isPrivate,
viewCount: details.viewCount || microformat.viewCount,
lengthSeconds: details.lengthSeconds || microformat.lengthSeconds,
likes: parseInt(
columnUI?.contents[1].slimVideoMetadataSectionRenderer?.contents[1].slimVideoActionBarRenderer?.buttons[0].slimMetadataToggleButtonRenderer?.button?.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(
/\D/g,
""
)
), // Yes. I know.
},
renderedData: {
description:
renderedPanels[0].engagementPanelSectionListRenderer?.content
.structuredDescriptionContentRenderer?.items[1]
.expandableVideoDescriptionBodyRenderer?.descriptionBodyText.runs,
recommendations:
columnUI?.contents[columnUI.contents.length - 1].itemSectionRenderer
?.contents,
},
};
}
}
export default Innertube
export default Innertube;

View file

@ -1,63 +1,89 @@
import Innertube from "./innertube";
import Innertube from "./innertube";
import constants from "./constants";
// Pointer object, give a key and it will return with a method
function useRender (video, renderer) {
switch(renderer) {
case "videoWithContextRenderer":
return videoWithContextRenderer(video)
case "gridVideoRenderer":
return gridVideoRenderer(video)
case "compactAutoplayRenderer":
return compactAutoplayRenderer(video)
default:
return undefined
}
function useRender(video, renderer) {
switch (renderer) {
case "videoWithContextRenderer":
return videoWithContextRenderer(video);
case "gridVideoRenderer":
return gridVideoRenderer(video);
case "compactAutoplayRenderer":
return compactAutoplayRenderer(video);
default:
return undefined;
}
}
function gridVideoRenderer(video) {
return {
id: video.videoId,
title: video.title?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0].text : video.longBylineText?.runs[0].text,
channelId: (video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.browseId,
channelURL: `${constants.YT_URL}/${(video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`,
channelThumbnail: video.channelThumbnail?.thumbnails[0],
metadata: {
published: video.publishedTimeText?.runs[0].text,
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),
},
};
return {
id: video.videoId,
title: video.title?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0]
? video.shortBylineText.runs[0].text
: video.longBylineText?.runs[0].text,
channelId: (video.shortBylineText?.runs[0]
? video.shortBylineText.runs[0]
: video.longBylineText?.runs[0]
).navigationEndpoint?.browseEndpoint?.browseId,
channelURL: `${constants.YT_URL}/${
(video.shortBylineText?.runs[0]
? video.shortBylineText.runs[0]
: video.longBylineText?.runs[0]
).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl
}`,
channelThumbnail: video.channelThumbnail?.thumbnails[0],
metadata: {
published: video.publishedTimeText?.runs[0].text,
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
),
},
};
}
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,
},
}
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;
if (video) item = video[0]
if (item) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0])
else return undefined
video = video.contents;
let item;
if (video) item = video[0];
if (item) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0]);
else return undefined;
}
export default useRender
export default useRender;

View file

@ -1,103 +1,99 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import constants from './constants';
import { Http } from "@capacitor-community/http";
import constants from "./constants";
function generateUserID(length = 36) {
const charset =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (crypto && crypto.getRandomValues) {
const values = new Uint32Array(length);
crypto.getRandomValues(values);
for (let i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
return result;
} else {
for (let i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
return result;
const charset =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (crypto && crypto.getRandomValues) {
const values = new Uint32Array(length);
crypto.getRandomValues(values);
for (let i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
return result;
} else {
for (let i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
return result;
}
}
function countLeadingZeroes(uInt8View, limit) {
let zeroes = 0;
let value = 0;
for (let i = 0; i < uInt8View.length; i++) {
value = uInt8View[i];
if (value === 0) {
zeroes += 8;
} else {
let count = 1;
if (value >>> 4 === 0) {
count += 4;
value <<= 4;
}
if (value >>> 6 === 0) {
count += 2;
value <<= 2;
}
zeroes += count - (value >>> 7);
break;
let zeroes = 0;
let value = 0;
for (let i = 0; i < uInt8View.length; i++) {
value = uInt8View[i];
if (value === 0) {
zeroes += 8;
} else {
let count = 1;
if (value >>> 4 === 0) {
count += 4;
value <<= 4;
}
if (zeroes >= limit) {
break;
if (value >>> 6 === 0) {
count += 2;
value <<= 2;
}
zeroes += count - (value >>> 7);
break;
}
if (zeroes >= limit) {
break;
}
return zeroes;
}
return zeroes;
}
//--- Puzzle Solver from Anarios --//
async function solvePuzzle(puzzle) {
let challenge = Uint8Array.from(atob(puzzle.challenge), (c) =>
c.charCodeAt(0)
);
let buffer = new ArrayBuffer(20);
let uInt8View = new Uint8Array(buffer);
let uInt32View = new Uint32Array(buffer);
let maxCount = Math.pow(2, puzzle.difficulty) * 5;
for (let i = 4; i < 20; i++) {
uInt8View[i] = challenge[i - 4];
}
for (let i = 0; i < maxCount; i++) {
uInt32View[0] = i;
let hash = await crypto.subtle.digest("SHA-512", buffer);
let hashUint8 = new Uint8Array(hash);
if (countLeadingZeroes(hashUint8) >= puzzle.difficulty) {
return {
solution: btoa(String.fromCharCode.apply(null, uInt8View.slice(0, 4))),
};
}
}
let challenge = Uint8Array.from(atob(puzzle.challenge), (c) =>
c.charCodeAt(0)
);
let buffer = new ArrayBuffer(20);
let uInt8View = new Uint8Array(buffer);
let uInt32View = new Uint32Array(buffer);
let maxCount = Math.pow(2, puzzle.difficulty) * 5;
for (let i = 4; i < 20; i++) {
uInt8View[i] = challenge[i - 4];
}
for (let i = 0; i < maxCount; i++) {
uInt32View[0] = i;
let hash = await crypto.subtle.digest("SHA-512", buffer);
let hashUint8 = new Uint8Array(hash);
if (countLeadingZeroes(hashUint8) >= puzzle.difficulty) {
return {
solution: btoa(String.fromCharCode.apply(null, uInt8View.slice(0, 4))),
};
}
}
}
const rydModule = {
logs: new Array(),
logs: new Array(),
//--- Get Dislikes ---//
getDislikes(id, callback) {
console.log("fetching ryd")
Http.request({
method: 'GET',
url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id }
})
.then((res) => {
callback(res.data)
})
.catch((err) => {
callback(err);
});
}
}
//--- Get Dislikes ---//
getDislikes(id, callback) {
console.log("fetching ryd");
Http.request({
method: "GET",
url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id },
})
.then((res) => {
callback(res.data);
})
.catch((err) => {
callback(err);
});
},
};
//--- Start ---//
export default ({ app }, inject) => {
inject('ryd', {...rydModule})
}
inject("ryd", { ...rydModule });
};

View file

@ -1,7 +1,10 @@
// Collection of functions that are useful but non-specific to any particular files
function getBetweenStrings(data, start_string, end_string) {
const regex = new RegExp(`${escapeRegExp(start_string)}(.*?)${escapeRegExp(end_string)}`, "s");
const regex = new RegExp(
`${escapeRegExp(start_string)}(.*?)${escapeRegExp(end_string)}`,
"s"
);
const match = data.match(regex);
return match ? match[1] : undefined;
}
@ -10,15 +13,15 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
function rgbToHex(r, g, b) {
@ -28,5 +31,5 @@ function rgbToHex(r, g, b) {
module.exports = {
getBetweenStrings,
hexToRgb,
rgbToHex
rgbToHex,
};

View file

@ -1,73 +1,71 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import { StatusBar, Style } from '@capacitor/status-bar';
import constants from './constants';
import { hexToRgb, rgbToHex } from './utils';
import { Http } from "@capacitor-community/http";
import { StatusBar, Style } from "@capacitor/status-bar";
import constants from "./constants";
import { hexToRgb, rgbToHex } from "./utils";
const module = {
//--- Get GitHub Commits ---//
commits: new Promise((resolve, reject) => {
Http.request({
method: "GET",
url: `${constants.URLS.VT_GITHUB}/commits`,
params: {},
})
.then((res) => {
resolve(res.data);
})
.catch((err) => {
reject(err);
});
}),
//--- Get GitHub Commits ---//
commits: new Promise((resolve, reject) => {
getRuns(item, callback) {
let url = `${constants.URLS.VT_GITHUB}/commits/${item.sha}/check-runs`;
Http.request({
method: 'GET',
url: `${constants.URLS.VT_GITHUB}/commits`,
params: {}
})
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err)
});
}),
getRuns(item, callback) {
let url = `${constants.URLS.VT_GITHUB}/commits/${item.sha}/check-runs`;
Http.request({
method: 'GET',
url: url,
params: {}
})
.then((res) => {
callback(res.data)
})
.catch((err) => {
callback(err)
});
Http.request({
method: "GET",
url: url,
params: {},
})
.then((res) => {
callback(res.data);
})
.catch((err) => {
callback(err);
});
},
statusBar: {
async hide() {
return await StatusBar.hide();
},
statusBar: {
async hide() {
return await StatusBar.hide();
},
async show() {
return await StatusBar.show();
},
async setLight() {
return await StatusBar.setStyle({ style: Style.Light });
},
async setDark() {
return await StatusBar.setStyle({ style: Style.Dark });
},
async setTransparent() {
return StatusBar.setOverlaysWebView({ overlay: true });
},
async setBackground(color) {
return await StatusBar.setBackgroundColor({color: color});
}
async show() {
return await StatusBar.show();
},
async setLight() {
return await StatusBar.setStyle({ style: Style.Light });
},
async setDark() {
return await StatusBar.setStyle({ style: Style.Dark });
},
async setTransparent() {
return StatusBar.setOverlaysWebView({ overlay: true });
},
async setBackground(color) {
return await StatusBar.setBackgroundColor({ color: color });
},
},
hexToRgb(hex) { return hexToRgb(hex); },
rgbToHex(r, g, b) { return rgbToHex(r, g, b); }
}
hexToRgb(hex) {
return hexToRgb(hex);
},
rgbToHex(r, g, b) {
return rgbToHex(r, g, b);
},
};
//--- Start ---//
export default ({ app }, inject) => {
inject('vuetube', module)
}
inject("vuetube", module);
};

View file

@ -1,172 +1,192 @@
//--- Modules/Imports ---//
import { Http } from '@capacitor-community/http';
import Innertube from './innertube'
import constants from './constants';
import useRender from './renderers';
import { Http } from "@capacitor-community/http";
import Innertube from "./innertube";
import constants from "./constants";
import useRender from "./renderers";
//--- Logger Function ---//
function logger(func, data, isError = false) {
searchModule.logs.unshift({
name: func,
time: Date.now(),
data: data,
error: isError
})
searchModule.logs.unshift({
name: func,
time: Date.now(),
data: data,
error: isError,
});
}
//--- Youtube Base Parser ---//
function youtubeParse(html, callback) {
//--- Replace Encoded Characters ---///
html = html.replace(/\\x([0-9A-F]{2})/ig, (...items) => { return String.fromCharCode(parseInt(items[1], 16)); });
//--- Properly Format JSON ---//
html = html.replaceAll("\\\\\"", "");
//--- Parse JSON ---//
html = JSON.parse(html);
//--- 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) {}
}
//--- 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];
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);
});
youtubeParse(html, (data) => {
callback(data);
});
})
.catch((err) => {
logger(constants.LOGGER_NAMES.search, err, true);
callback(err);
});
}
const searchModule = {
logs: new Array(),
logs: new Array(),
//--- Get YouTube's Search Auto Complete ---//
autoComplete(text, callback) {
Http.request({
method: 'GET',
url: `${constants.URLS.YT_SUGGESTIONS}/search`,
params: { client: 'youtube', q: text }
})
.then((res) => {
logger(constants.LOGGER_NAMES.autoComplete, res);
callback(res.data);
})
.catch((err) => {
logger(constants.LOGGER_NAMES.autoComplete, err, true);
callback(err);
});
},
//--- Get YouTube's Search Auto Complete ---//
autoComplete(text, callback) {
Http.request({
method: "GET",
url: `${constants.URLS.YT_SUGGESTIONS}/search`,
params: { client: "youtube", q: text },
})
.then((res) => {
logger(constants.LOGGER_NAMES.autoComplete, res);
callback(res.data);
})
.catch((err) => {
logger(constants.LOGGER_NAMES.autoComplete, err, true);
callback(err);
});
},
search(text, callback) {
search(text, callback) {
let results = new Array();
youtubeSearch(text, (videos) => {
for (const i in videos) {
const video = videos[i];
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() {
return this.replace(/\\x([0-9A-Fa-f]{2})/g, function() {
return String.fromCharCode(parseInt(arguments[1], 16));
});
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,
};
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);
});
logger("vidData", data);
callback(data);
})
.catch((err) => {
logger("codeRun", err, true);
callback(err);
});
},
},
getReturnYoutubeDislike(id, callback) {
Http.request({
method: 'GET',
url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id }
})
.then((res) => {
logger("rydData", res.data)
callback(res.data)
})
.catch((err) => {
logger("codeRun", err, true);
callback(err);
});
}
}
getReturnYoutubeDislike(id, callback) {
Http.request({
method: "GET",
url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id },
})
.then((res) => {
logger("rydData", res.data);
callback(res.data);
})
.catch((err) => {
logger("codeRun", err, true);
callback(err);
});
},
};
//--- Recommendations ---//
@ -175,58 +195,71 @@ let InnertubeAPI;
// Loads Innertube object. This will be the object used in all future Innertube API calls. getAPI Code provided by Lightfire228 (https://github.com/Lightfire228)
// These are just a way for the backend Javascript to communicate with the front end Vue scripts. Essentially a wrapper inside a wrapper
const innertubeModule = {
async getAPI() {
if (!InnertubeAPI) {
InnertubeAPI = await Innertube.createAsync((message, isError) => {
logger(constants.LOGGER_NAMES.innertube, message, isError);
});
}
return InnertubeAPI;
},
async getAPI() {
if (!InnertubeAPI) {
InnertubeAPI = await Innertube.createAsync((message, isError) => { logger(constants.LOGGER_NAMES.innertube, message, isError); })
async getVid(id) {
try {
return await InnertubeAPI.VidInfoAsync(id);
} catch (error) {
logger(constants.LOGGER_NAMES.watch, error, true);
}
},
// It just works™
// Front page recommendation
async recommend() {
const response = await InnertubeAPI.getRecommendationsAsync();
if (!response.success)
throw new Error("An error occurred and innertube failed to respond");
const contents =
response.data.contents.singleColumnBrowseResultsRenderer.tabs[0]
.tabRenderer.content.sectionListRenderer.contents;
const final = contents.map((shelves) => {
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;
}
});
});
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;
}
return InnertubeAPI;
},
async getVid(id) {
try {
return await InnertubeAPI.VidInfoAsync(id)
} catch (error) {
logger(constants.LOGGER_NAMES.watch, error, true)
}
},
// It just works™
// Front page recommendation
async recommend() {
const response = await InnertubeAPI.getRecommendationsAsync();
if (!response.success) throw new Error("An error occurred and innertube failed to respond")
const contents = response.data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
const final = contents.map((shelves) => {
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}
})
})
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 ---//
export default ({ app }, inject) => {
inject('youtube', {...searchModule, ...innertubeModule })
inject("logger", logger)
}
inject("youtube", { ...searchModule, ...innertubeModule });
inject("logger", logger);
};
logger(constants.LOGGER_NAMES.init, "Program Started");

View file

@ -1,18 +1,18 @@
export const state = () => ({
roundTweak: 0
})
roundTweak: 0,
});
export const mutations = {
initTweaks(state) {
// NOTE: localStorage is not reactive, so it will only be used on first load
// currently called beforeCreate() in pages/default.vue
if (process.client) {
state.roundTweak = localStorage.getItem("roundTweak") || 0
}
},
setRoundTweak (state, payload) {
if (!isNaN(payload)) {
state.roundTweak = payload
localStorage.setItem("roundTweak", payload)
}
initTweaks(state) {
// NOTE: localStorage is not reactive, so it will only be used on first load
// currently called beforeCreate() in pages/default.vue
if (process.client) {
state.roundTweak = localStorage.getItem("roundTweak") || 0;
}
}
},
setRoundTweak(state, payload) {
if (!isNaN(payload)) {
state.roundTweak = payload;
localStorage.setItem("roundTweak", payload);
}
},
};

1
scripts/install.sh Normal file
View file

@ -0,0 +1 @@
npm i; cd NUXT/; npm i; cd ..;