Start from scratch

This commit is contained in:
root
2021-07-20 13:05:19 +05:30
parent 960c7b88a2
commit 8719dfbe12
51 changed files with 2048 additions and 4600 deletions
+5
View File
@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+1
View File
@@ -2,6 +2,7 @@
node_modules
/dist
# local env files
.env.local
.env.*.local
+58 -57
View File
@@ -1,62 +1,63 @@
{
"name": "piped",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"name": "piped-redesign",
"version": "0.1.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vuetify": "^2.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"lint-staged": "^9.5.0",
"sass": "~1.32.0",
"sass-loader": "^10.0.0",
"vue-cli-plugin-vuetify": "~2.4.1",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.7.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"core-js": "^3.15.2",
"css-loader": "^6.1.0",
"dompurify": "^2.3.0",
"hotkeys-js": "^3.8.7",
"javascript-time-ago": "^2.3.8",
"lodash-es": "^4.17.21",
"mux.js": "^5.12.2",
"register-service-worker": "^1.7.1",
"shaka-player": "3.2.0",
"uikit": "3.7.1",
"vue": "^3.1.5",
"vue-router": "^4.0.10",
"xml-js": "^1.6.11"
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"@vue/compiler-sfc": "^3.1.5",
"@vue/eslint-config-standard": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.31.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.14.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1% in IN",
"> 1% in US"
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"gitHooks": {
"pre-commit": "lint-staged"
},
"license": "AGPL-3.0-or-later",
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

+17 -24
View File
@@ -1,26 +1,19 @@
<!DOCTYPE html>
<html style="background: #0b0e0f" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<link
title="Piped"
type="application/opensearchdescription+xml"
rel="search"
href="<%= BASE_URL %>opensearch.xml"
/>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Piped</ShortName>
<LongName>Piped Search</LongName>
<Description>Search for videos, channels, and playlists on Piped</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="48" height="48" type="image/x-icon">https://piped.kavin.rocks/favicon.ico</Image>
<Url type="text/html" method="get" template="https://piped.kavin.rocks/results?search_query={searchTerms}" />
</OpenSearchDescription>
-2
View File
@@ -1,2 +0,0 @@
User-agent: *
Disallow:
+49 -71
View File
@@ -1,82 +1,60 @@
<template>
<div
class="uk-container uk-container-expand uk-height-viewport"
:style="[{ background: backgroundColor, colour: foregroundColor }]"
:class="{ 'uk-light': darkMode }"
<v-app>
<v-app-bar
app
color="primary"
dark
>
<Navigation />
<router-view v-slot="{ Component }">
<keep-alive :max="5">
<component :key="$route.fullPath" :is="Component" />
</keep-alive>
</router-view>
<div class="d-flex align-center">
<v-img
alt="Vuetify Logo"
class="shrink mr-2"
contain
src="https://cdn.vuetifyjs.com/images/logos/vuetify-logo-dark.png"
transition="scale-transition"
width="40"
/>
<div style="text-align: center">
<a aria-label="GitHub" href="https://github.com/TeamPiped/Piped">
<font-awesome-icon :icon="['fab', 'github']"></font-awesome-icon>
</a>
&nbsp;
<a href="https://github.com/TeamPiped/Piped#donations">
<font-awesome-icon :icon="['fab', 'bitcoin']"></font-awesome-icon>
Donations
</a>
</div>
</div>
<v-img
alt="Vuetify Name"
class="shrink mt-1 hidden-sm-and-down"
contain
min-width="100"
src="https://cdn.vuetifyjs.com/images/logos/vuetify-name-dark.png"
width="100"
/>
</div>
<v-spacer></v-spacer>
<v-btn
href="https://github.com/vuetifyjs/vuetify/releases/latest"
target="_blank"
text
>
<span class="mr-2">Latest Release</span>
<v-icon>mdi-open-in-new</v-icon>
</v-btn>
</v-app-bar>
<v-main>
<HelloWorld/>
</v-main>
</v-app>
</template>
<script>
import Navigation from '@/components/Navigation'
import HelloWorld from './components/HelloWorld'
export default {
name: 'App',
components: {
Navigation
HelloWorld
},
mounted () {
if (window.location.pathname === '/' || window.location.pathname.length == 0) {
switch (this.getPreferenceString('homepage', 'trending')) {
case 'trending':
break
case 'feed':
this.$router.replace('/feed')
break
default:
break
}
}
}
data: () => ({
//
})
}
</script>
<style>
h1,
p,
a,
b {
unicode-bidi: plaintext;
text-align: start;
}
::-webkit-scrollbar {
background-color: #15191a;
color: #c5bcae;
}
::-webkit-scrollbar-thumb {
background-color: #4b4f52;
}
::-webkit-scrollbar-thumb:hover {
background-color: #5b6469;
}
::-webkit-scrollbar-thumb:active {
background-color: #485053;
}
::-webkit-scrollbar-corner {
background-color: #0b0e0f;
}
* {
scrollbar-color: #15191a #444a4e;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

After

Width:  |  Height:  |  Size: 539 B

-116
View File
@@ -1,116 +0,0 @@
<template>
<ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" />
<div v-if="channel" v-show="!channel.error">
<h1 class="uk-text-center"><img height="48" width="48" v-bind:src="channel.avatarUrl" />{{ channel.name }}</h1>
<img v-if="channel.bannerUrl" v-bind:src="channel.bannerUrl" style="width: 100%" loading="lazy" />
<p style="white-space: pre-wrap">{{ channel.description }}</p>
<button
v-if="authenticated"
@click="subscribeHandler"
class="uk-button uk-button-small"
style="background: #222"
type="button"
>
{{ subscribed ? "Unsubscribe" : "Subscribe" }}
</button>
<hr />
<div class="uk-grid-xl" uk-grid="parallax: 0">
<div
class="uk-width-1-2 uk-width-1-3@m uk-width-1-4@l uk-width-1-5@xl"
v-bind:key="video.url"
v-for="video in this.channel.relatedStreams"
>
<VideoItem :video="video" height="94" width="168" hideChannel />
</div>
</div>
</div>
</template>
<script>
import ErrorHandler from '@/components/ErrorHandler.vue'
import VideoItem from '@/components/VideoItem.vue'
export default {
data () {
return {
channel: null,
subscribed: false
}
},
mounted () {
this.getChannelData()
},
activated () {
window.addEventListener('scroll', this.handleScroll)
},
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
},
methods: {
async fetchSubscribedStatus () {
this.fetchJson(
this.apiUrl() + '/subscribed',
{
channelId: this.channel.id
},
{
headers: {
Authorization: this.getAuthToken()
}
}
).then(json => {
this.subscribed = json.subscribed
})
},
async fetchChannel () {
const url = this.apiUrl() + '/' + this.$route.params.path + '/' + this.$route.params.channelId
return await this.fetchJson(url)
},
async getChannelData () {
this.fetchChannel()
.then(data => (this.channel = data))
.then(() => {
if (!this.channel.error) {
document.title = this.channel.name + ' - Piped'
if (this.authenticated) this.fetchSubscribedStatus()
}
})
},
handleScroll () {
if (this.loading || !this.channel || !this.channel.nextpage) return
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - window.innerHeight) {
this.loading = true
this.fetchJson(this.apiUrl() + '/nextpage/channel/' + this.channel.id, {
nextpage: this.channel.nextpage
}).then(json => {
this.channel.relatedStreams.concat(json.relatedStreams)
this.channel.nextpage = json.nextpage
this.loading = false
json.relatedStreams.map(stream => this.channel.relatedStreams.push(stream))
})
}
},
subscribeHandler () {
this.fetchJson(this.apiUrl() + (this.subscribed ? '/unsubscribe' : '/subscribe'), null, {
method: 'POST',
body: JSON.stringify({
channelId: this.channel.id
}),
headers: {
Authorization: this.getAuthToken(),
'Content-Type': 'application/json'
}
})
this.subscribed = !this.subscribed
}
},
components: {
ErrorHandler,
VideoItem
}
}
</script>
-16
View File
@@ -1,16 +0,0 @@
<template>
<p>{{ message }}</p>
<button uk-toggle="target: #stacktrace" class="uk-button uk-button-small" style="background: #222" type="button">
Show More
</button>
<p id="stacktrace" style="white-space: pre-wrap" hidden>{{ error }}</p>
</template>
<script>
export default {
props: {
error: String,
message: String
}
}
</script>
-83
View File
@@ -1,83 +0,0 @@
<template>
<h1 class="uk-text-bold uk-text-center">Feed</h1>
<small>You can import subscriptions from <router-link to="/import">here</router-link>.</small>
<div class="uk-align-right">
<a :href="getRssUrl"><font-awesome-icon icon="rss"></font-awesome-icon></a>
</div>
<hr />
<div class="uk-grid-xl" uk-grid="parallax: 0">
<div
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
v-bind:key="video.url"
v-for="video in videos"
>
<div class="uk-text-secondary" :style="[{ background: backgroundColor }]">
<router-link class="uk-text-emphasis" v-bind:to="'/watch?v=' + video.id">
<img style="width: 100%" v-bind:src="video.thumbnail" alt="thumbnail" loading="lazy" />
<p>{{ video.title }}</p>
</router-link>
<div>
<div>
<router-link class="uk-link-muted" :to="'/channel/' + video.uploader_id">
<a>{{ video.uploader }}</a>
</router-link>
<br />
</div>
</div>
<b class="uk-text-small uk-align-left">
<div v-if="video.views >= 0">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ numberFormat(video.views) }} views
<br />
</div>
<div>
{{ timeAgo(video.uploaded) }}
</div>
</b>
<div class="uk-align-right">
<b class="uk-text-small">{{ timeFormat(video.duration) }}</b>
<br />
<router-link :to="'/watch?v=' + video.id + '&listen=1'">
<font-awesome-icon icon="headphones"></font-awesome-icon>
</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
videos: []
}
},
mounted () {
document.title = 'Feed - Piped'
this.fetchFeed().then(videos => (this.videos = videos))
},
methods: {
async fetchFeed () {
return await this.fetchJson(this.apiUrl() + '/feed', {
authToken: this.getAuthToken()
})
}
},
computed: {
getRssUrl (_this) {
return _this.apiUrl() + '/feed/rss?authToken=' + _this.getAuthToken()
}
}
}
</script>
+151
View File
@@ -0,0 +1,151 @@
<template>
<v-container>
<v-row class="text-center">
<v-col cols="12">
<v-img
:src="require('../assets/logo.svg')"
class="my-3"
contain
height="200"
/>
</v-col>
<v-col class="mb-4">
<h1 class="display-2 font-weight-bold mb-3">
Welcome to Vuetify
</h1>
<p class="subheading font-weight-regular">
For help and collaboration with other Vuetify developers,
<br>please join our online
<a
href="https://community.vuetifyjs.com"
target="_blank"
>Discord Community</a>
</p>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
What's next?
</h2>
<v-row justify="center">
<a
v-for="(next, i) in whatsNext"
:key="i"
:href="next.href"
class="subheading mx-3"
target="_blank"
>
{{ next.text }}
</a>
</v-row>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Important Links
</h2>
<v-row justify="center">
<a
v-for="(link, i) in importantLinks"
:key="i"
:href="link.href"
class="subheading mx-3"
target="_blank"
>
{{ link.text }}
</a>
</v-row>
</v-col>
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Ecosystem
</h2>
<v-row justify="center">
<a
v-for="(eco, i) in ecosystem"
:key="i"
:href="eco.href"
class="subheading mx-3"
target="_blank"
>
{{ eco.text }}
</a>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'HelloWorld',
data: () => ({
ecosystem: [
{
text: 'vuetify-loader',
href: 'https://github.com/vuetifyjs/vuetify-loader'
},
{
text: 'github',
href: 'https://github.com/vuetifyjs/vuetify'
},
{
text: 'awesome-vuetify',
href: 'https://github.com/vuetifyjs/awesome-vuetify'
}
],
importantLinks: [
{
text: 'Documentation',
href: 'https://vuetifyjs.com'
},
{
text: 'Chat',
href: 'https://community.vuetifyjs.com'
},
{
text: 'Made with Vuetify',
href: 'https://madewithvuejs.com/vuetify'
},
{
text: 'Twitter',
href: 'https://twitter.com/vuetifyjs'
},
{
text: 'Articles',
href: 'https://medium.com/vuetify'
}
],
whatsNext: [
{
text: 'Explore components',
href: 'https://vuetifyjs.com/components/api-explorer'
},
{
text: 'Select a layout',
href: 'https://vuetifyjs.com/getting-started/pre-made-layouts'
},
{
text: 'Frequently Asked Questions',
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
}
]
})
}
</script>
-129
View File
@@ -1,129 +0,0 @@
<template>
<div class="uk-vertical-align uk-text-center uk-height-1-1 ">
<form class="uk-panel uk-panel-box">
<div class="uk-form-row">
<input type="file" @change="fileChange" ref="fileSelector" />
</div>
<div class="uk-form-row">
<b>Selected Subscriptions: {{ selectedSubscriptions }}</b>
</div>
<div class="uk-form-row">
<a
class="uk-width-1-1 uk-button uk-button-primary uk-button-large uk-width-auto"
style="background: #222"
@click="handleImport"
>Import</a
>
</div>
</form>
<br />
<b>Importing Subscriptions from YouTube</b>
<br />
<div>
Open
<a href="https://takeout.google.com/takeout/custom/youtube">takeout.google.com/takeout/custom/youtube</a>
<br />
In "Select data to include", click on "All YouTube data included" and select only "subscriptions".
<br />
Create the export and download the zip file.
<br />
Extract subscriptions.json from the zip file.
<br />
Select and import the file above.
</div>
<br />
<b>Importing Subscriptions from Invidious</b>
<br />
<div>
Open
<a href="https://invidio.us/data_control">invidiou.us/data_control</a>
<br />
Click on any of the export options.
<br />
Select and import the file above.
</div>
<br />
<b>Importing Subscriptions from NewPipe</b>
<br />
<div>
Go to the Feed tab.
<br />
Click on the arrow on where it says "Subscriptions".
<br />
Save the file somewhere.
<br />
Select and import the file above.
</div>
</div>
</template>
<script>
export default {
data () {
return {
subscriptions: []
}
},
computed: {
selectedSubscriptions () {
return this.subscriptions.length
}
},
activated () {
if (!this.authenticated) this.$router.push('/login')
},
methods: {
fileChange () {
this.$refs.fileSelector.files[0].text().then(text => {
this.subscriptions = []
// Invidious
if (text.indexOf('opml') !== -1) {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(text, 'text/xml')
xmlDoc.querySelectorAll('outline[xmlUrl]').forEach(item => {
const url = item.getAttribute('xmlUrl')
const id = url.substr(-24)
this.subscriptions.push(id)
})
}
// NewPipe
if (text.indexOf('app_version') !== -1) {
const json = JSON.parse(text)
json.subscriptions
.filter(item => item.service_id === 0)
.forEach(item => {
const url = item.url
const id = url.substr(-24)
this.subscriptions.push(id)
})
}
// Invidious JSON
if (text.indexOf('thin_mode') !== -1) {
const json = JSON.parse(text)
this.subscriptions = json.subscriptions
}
// Google Takeout
if (text.indexOf('contentDetails') !== -1) {
const json = JSON.parse(text)
json.forEach(item => {
const id = item.snippet.resourceId.channelId
this.subscriptions.push(id)
})
}
})
},
handleImport () {
this.fetchJson(this.apiUrl() + '/import', null, {
method: 'POST',
headers: {
Authorization: this.getAuthToken()
},
body: JSON.stringify(this.subscriptions)
}).then(json => {
if (json.message === 'ok') window.location = '/feed'
})
}
}
}
</script>
-66
View File
@@ -1,66 +0,0 @@
<template>
<div class="uk-vertical-align uk-text-center uk-height-1-1 ">
<form class="uk-panel uk-panel-box">
<div class="uk-form-row">
<input
class="uk-width-1-1 uk-form-large uk-input uk-width-auto"
type="text"
v-model="username"
autocomplete="username"
placeholder="Username"
/>
</div>
<div class="uk-form-row">
<input
class="uk-width-1-1 uk-form-large uk-input uk-width-auto"
type="password"
v-model="password"
autocomplete="password"
placeholder="Password"
/>
</div>
<div class="uk-form-row">
<a
class="uk-width-1-1 uk-button uk-button-primary uk-button-large uk-width-auto"
style="background: #222"
@click="login"
>Login</a
>
</div>
</form>
</div>
</template>
<script>
export default {
data () {
return {
username: null,
password: null
}
},
mounted () {
// TODO: Add Server Side check
if (this.getAuthToken()) {
this.$router.push('/')
}
},
methods: {
login () {
console.log('authToken' + this.hashCode(this.apiUrl()))
this.fetchJson(this.apiUrl() + '/login', null, {
method: 'POST',
body: JSON.stringify({
username: this.username,
password: this.password
})
}).then(resp => {
if (resp.token) {
this.setPreference('authToken' + this.hashCode(this.apiUrl()), resp.token)
window.location = '/' // done to bypass cache
} else alert(resp.error)
})
}
}
}
</script>
-104
View File
@@ -1,104 +0,0 @@
<template>
<nav
class="uk-navbar-container uk-container-expand uk-position-relative"
:style="[{ background: backgroundColor, colour: foregroundColor }]"
uk-navbar
>
<div class="uk-navbar-left">
<router-link class="uk-navbar-item uk-logo uk-text-bold" :style="[{ colour: foregroundColor }]" to="/"
><img alt="logo" src="/img/icons/logo.svg" height="32" width="32" style="margin-bottom: 6px; margin-right: -13px" />iped</router-link
>
</div>
<div class="uk-navbar-center uk-flex uk-visible@m">
<input
class="uk-input uk-width-medium"
type="text"
placeholder="Search"
v-model="searchText"
@keyup="onKeyUp"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</div>
<div class="uk-navbar-right">
<ul class="uk-navbar-nav">
<li>
<router-link to="/preferences">Preferences</router-link>
</li>
<li v-if="shouldShowLogin">
<router-link to="/login">Login</router-link>
</li>
<li v-if="shouldShowLogin">
<router-link to="/register">Register</router-link>
</li>
<li v-if="authenticated">
<router-link to="/feed">Feed</router-link>
</li>
</ul>
</div>
</nav>
<div class="uk-container-expand uk-hidden@m">
<input
class="uk-input"
type="text"
placeholder="Search"
v-model="searchText"
@keyup="onKeyUp"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</div>
<SearchSuggestions
v-show="searchText && suggestionsVisible"
:searchText="searchText"
@searchchange="onSearchTextChange"
ref="searchSuggestions"
/>
</template>
<script>
import SearchSuggestions from '@/components/SearchSuggestions'
export default {
components: {
SearchSuggestions
},
data () {
return {
searchText: '',
suggestionsVisible: false
}
},
computed: {
shouldShowLogin (_this) {
return _this.getAuthToken() == null
}
},
methods: {
onKeyUp (e) {
if (e.key === 'Enter') {
e.target.blur()
this.$router.push({
name: 'SearchResults',
query: { search_query: this.searchText }
})
return
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault()
}
this.$refs.searchSuggestions.onKeyUp(e)
},
onInputFocus () {
this.suggestionsVisible = true
},
onInputBlur () {
this.suggestionsVisible = false
},
onSearchTextChange (searchText) {
this.searchText = searchText
}
}
}
</script>
<style></style>
-271
View File
@@ -1,271 +0,0 @@
<template>
<div class="uk-container-expand">
<div
data-shaka-player-container
style="width: 100%; height: 100%; max-height: 75vh; min-height: 250px; background: #000"
ref="container"
>
<video
data-shaka-player
class="uk-width-expand"
:autoplay="shouldAutoPlay"
:loop="selectedAutoLoop"
ref="videoEl"
></video>
</div>
</div>
</template>
<script>
import muxjs from 'mux.js'
import shaka from 'shaka-player/dist/shaka-player.ui.js'
import('shaka-player/dist/controls.css')
window.muxjs = muxjs
export default {
props: {
video: Object,
sponsors: Object,
selectedAutoPlay: Boolean,
selectedAutoLoop: Boolean
},
data () {
return {
player: null
}
},
computed: {
shouldAutoPlay: _this => {
return _this.getPreferenceBoolean('playerAutoPlay', true)
}
},
mounted () {
if (!this.shaka) this.shakaPromise = shaka.then(shaka => shaka.default).then(shaka => (this.shaka = shaka))
},
methods: {
loadVideo () {
const component = this
const videoEl = this.$refs.videoEl
videoEl.setAttribute('poster', this.video.thumbnailUrl)
if (this.$route.query.t) videoEl.currentTime = this.$route.query.t
const noPrevPlayer = !this.player
const streams = []
streams.push(...this.video.audioStreams)
streams.push(...this.video.videoStreams)
const MseSupport = window.MediaSource !== undefined
let uri
if (this.video.livestream) {
uri = this.video.hls
} else if (this.video.audioStreams.length > 0 && MseSupport) {
const dash = require('@/utils/DashUtils.js').default.generate_dash_file_from_formats(
streams,
this.video.duration
)
uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash)
} else {
uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url
}
if (noPrevPlayer) {
this.shakaPromise.then(() => {
this.shaka.polyfill.installAll()
const localPlayer = new this.shaka.Player(videoEl)
localPlayer.getNetworkingEngine().registerRequestFilter((_type, request) => {
const uri = request.uris[0]
const url = new URL(uri)
if (url.host.endsWith('.googlevideo.com')) {
url.searchParams.set('host', url.host)
url.host = new URL(component.video.proxyUrl).host
request.uris[0] = url.toString()
}
})
localPlayer.configure(
'streaming.bufferingGoal',
Math.max(this.getPreferenceNumber('bufferGoal', 10), 10)
)
this.setPlayerAttrs(localPlayer, videoEl, uri, this.shaka)
})
} else this.setPlayerAttrs(this.player, videoEl, uri, this.shaka)
if (noPrevPlayer) {
videoEl.addEventListener('timeupdate', () => {
if (this.sponsors && this.sponsors.segments) {
const time = videoEl.currentTime
this.sponsors.segments.forEach(segment => {
if (!segment.skipped || this.selectedAutoLoop) {
const end = segment.segment[1]
if (time >= segment.segment[0] && time < end) {
console.log('Skipped segment at ' + time)
videoEl.currentTime = end
segment.skipped = true
}
}
})
}
})
videoEl.addEventListener('volumechange', () => {
this.setPreference('volume', videoEl.volume)
})
videoEl.addEventListener('ended', () => {
if (!this.selectedAutoLoop && this.selectedAutoPlay && this.video.relatedStreams.length > 0) {
const params = this.$route.query
let url = this.video.relatedStreams[0].url
const searchParams = new URLSearchParams()
for (const param in params) {
switch (param) {
case 'v':
case 't':
break
default:
searchParams.set(param, params[param])
break
}
}
const paramStr = searchParams.toString()
if (paramStr.length > 0) url += '&' + paramStr
this.$router.push(url)
}
})
}
// TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
},
setPlayerAttrs (localPlayer, videoEl, uri, shaka) {
if (!this.ui) {
this.ui = new shaka.ui.Overlay(localPlayer, this.$refs.container, videoEl)
const config = {
overflowMenuButtons: ['quality', 'captions', 'picture_in_picture', 'playback_rate'],
seekBarColors: {
base: 'rgba(255, 255, 255, 0.3)',
buffered: 'rgba(255, 255, 255, 0.54)',
played: 'rgb(255, 0, 0)'
}
}
this.ui.configure(config)
}
const player = this.ui.getControls().getPlayer()
this.player = player
const disableVideo = this.getPreferenceBoolean('listen', false) && !this.video.livestream
this.player.configure({
preferredVideoCodecs: ['av01', 'vp9', 'avc1'],
preferredAudioCodecs: ['opus', 'mp4a'],
manifest: {
disableVideo: disableVideo
}
})
const quality = this.getPreferenceNumber('quality', 0)
const qualityConds =
quality > 0 && (this.video.audioStreams.length > 0 || this.video.livestream) && !disableVideo
if (qualityConds) this.player.configure('abr.enabled', false)
player.load(uri, 0, uri.indexOf('dash+xml') >= 0 ? 'application/dash+xml' : 'video/mp4').then(() => {
if (qualityConds) {
let leastDiff = Number.MAX_VALUE
let bestStream = null
player
.getVariantTracks()
.sort((a, b) => a.bandwidth - b.bandwidth)
.forEach(stream => {
const diff = Math.abs(quality - stream.height)
if (diff < leastDiff) {
leastDiff = diff
bestStream = stream
}
})
player.selectVariantTrack(bestStream)
}
this.video.subtitles.forEach(subtitle => {
player.addTextTrackAsync(
subtitle.url,
subtitle.code,
'SUBTITLE',
subtitle.mimeType,
null,
subtitle.name
)
})
videoEl.volume = this.getPreferenceNumber('volume', 1)
})
}
},
activated () {
import('hotkeys-js')
.then(mod => mod.default)
.then(hotkeys => {
this.hotkeys = hotkeys
const self = this
hotkeys('f,m,space,up,down,left,right', function (e, handler) {
const videoEl = self.$refs.videoEl
switch (handler.key) {
case 'f':
if (document.fullscreenElement) document.exitFullscreen()
else self.$refs.container.requestFullscreen()
e.preventDefault()
break
case 'm':
videoEl.muted = !videoEl.muted
e.preventDefault()
break
case 'space':
if (videoEl.paused) videoEl.play()
else videoEl.pause()
e.preventDefault()
break
case 'up':
videoEl.volume = Math.min(videoEl.volume + 0.05, 1)
e.preventDefault()
break
case 'down':
videoEl.volume = Math.max(videoEl.volume - 0.05, 0)
e.preventDefault()
break
case 'left':
videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0)
e.preventDefault()
break
case 'right':
videoEl.currentTime = videoEl.currentTime + 5
e.preventDefault()
break
}
})
})
},
deactivated () {
if (this.ui) {
this.ui.destroy()
this.ui = undefined
this.player = undefined
}
if (this.player) {
this.player.destroy()
this.player = undefined
}
if (this.hotkeys) this.hotkeys.unbind()
this.$refs.container.querySelectorAll('div').forEach(node => node.remove())
}
}
</script>
-90
View File
@@ -1,90 +0,0 @@
<template>
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
<div v-if="playlist" v-show="!playlist.error">
<h1 class="uk-text-center">
<img v-bind:src="playlist.avatarUrl" height="48" width="48" loading="lazy" />
{{ playlist.name }}
</h1>
<b
><router-link class="uk-text-justify" v-bind:to="playlist.uploaderUrl || '/'">
<img v-bind:src="playlist.uploaderAvatar" loading="lazy" />
{{ playlist.uploader }}</router-link
></b
>
<div class="uk-align-right">
<b>{{ playlist.videos }} Videos</b>
<br />
<a :href="getRssUrl"><font-awesome-icon icon="rss"></font-awesome-icon></a>
</div>
<hr />
<div class="uk-grid-xl" uk-grid="parallax: 0">
<div
class="uk-width-1-2 uk-width-1-3@m uk-width-1-4@l uk-width-1-5@xl"
v-bind:key="video.url"
v-for="video in this.playlist.relatedStreams"
>
<VideoItem :video="video" height="94" width="168" />
</div>
</div>
</div>
</template>
<script>
import ErrorHandler from '@/components/ErrorHandler.vue'
import VideoItem from '@/components/VideoItem.vue'
export default {
data () {
return {
playlist: null
}
},
mounted () {
this.getPlaylistData()
},
activated () {
window.addEventListener('scroll', this.handleScroll)
},
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
},
computed: {
getRssUrl: _this => {
return _this.apiUrl() + '/rss/playlists/' + _this.$route.query.list
}
},
methods: {
async fetchPlaylist () {
return await await this.fetchJson(this.apiUrl() + '/playlists/' + this.$route.query.list)
},
async getPlaylistData () {
this.fetchPlaylist()
.then(data => (this.playlist = data))
.then(() => (document.title = this.playlist.name + ' - Piped'))
},
handleScroll () {
if (this.loading || !this.playlist || !this.playlist.nextpage) return
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - window.innerHeight) {
this.loading = true
this.fetchJson(this.apiUrl() + '/nextpage/playlists/' + this.$route.query.list, {
nextpage: this.playlist.nextpage
}).then(json => {
this.playlist.relatedStreams.concat(json.relatedStreams)
this.playlist.nextpage = json.nextpage
this.loading = false
json.relatedStreams.map(stream => this.playlist.relatedStreams.push(stream))
})
}
}
},
components: {
ErrorHandler,
VideoItem
}
}
</script>
-250
View File
@@ -1,250 +0,0 @@
<template>
<h1 class="uk-text-bold uk-text-center">Preferences</h1>
<hr />
<h2>SponsorBlock</h2>
<p>Uses the API from <a href="https://sponsor.ajay.app/">sponsor.ajay.app</a></p>
<b>Enable Sponsorblock</b>
<br />
<input class="uk-checkbox" v-model="sponsorBlock" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Sponsors</b>
<br />
<input class="uk-checkbox" v-model="skipSponsor" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Intermission/Intro Animation</b>
<br />
<input class="uk-checkbox" v-model="skipIntro" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Endcards/Credits</b>
<br />
<input class="uk-checkbox" v-model="skipOutro" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Preview/Recap</b>
<br />
<input class="uk-checkbox" v-model="skipPreview" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Interaction Reminder (Subscribe)</b>
<br />
<input class="uk-checkbox" v-model="skipInteraction" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Unpaid/Self Promotion</b>
<br />
<input class="uk-checkbox" v-model="skipSelfPromo" @change="onChange($event)" type="checkbox" />
<br />
<b>Skip Music: Non-Music Section</b>
<br />
<input class="uk-checkbox" v-model="skipMusicOffTopic" @change="onChange($event)" type="checkbox" />
<br />
<b>Theme</b>
<br />
<select class="uk-select uk-width-auto" v-model="selectedTheme" @change="onChange($event)">
<option value="auto">Auto</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
<br />
<b>Autoplay Video</b>
<br />
<input class="uk-checkbox" v-model="autoPlayVideo" @change="onChange($event)" type="checkbox" />
<br />
<b>Audio Only</b>
<br />
<input class="uk-checkbox" v-model="listen" @change="onChange($event)" type="checkbox" />
<br />
<b>Default Quality</b>
<br />
<select class="uk-select uk-width-auto" v-model="defaultQuality" @change="onChange($event)">
<option value="0">Auto</option>
<option :key="resolution" v-for="resolution in resolutions" :value="resolution">{{ resolution }}p</option>
</select>
<br />
<b>Buffering Goal</b>
<br />
<input class="uk-input uk-width-auto" v-model="bufferingGoal" @change="onChange($event)" type="text" />
<br />
<b>Country Selection</b>
<br />
<select class="uk-select uk-width-auto" v-model="country" @change="onChange($event)">
<option :key="country.code" v-for="country in countryMap" :value="country.code">{{ country.name }}</option>
</select>
<br />
<b>Default Homepage</b>
<br />
<select class="uk-select uk-width-auto" v-model="defaultHomepage" @change="onChange($event)">
<option value="trending">Trending</option>
<option value="feed">Feed</option>
</select>
<br />
<b>Show Comments</b>
<br />
<input class="uk-checkbox" v-model="showComments" @change="onChange($event)" type="checkbox" />
<h2>Instances List</h2>
<table class="uk-table">
<thead>
<tr>
<th>Instance Name</th>
<th>Instance Locations</th>
<th>Has CDN?</th>
<th>SSL Score</th>
</tr>
</thead>
<tbody v-bind:key="instance.name" v-for="instance in instances">
<tr>
<td>{{ instance.name }}</td>
<td>{{ instance.locations }}</td>
<td>{{ instance.cdn }}</td>
<td>
<a :href="sslScore(instance.apiurl)" target="_blank">Click Here</a>
</td>
</tr>
</tbody>
</table>
<hr />
<b>Instance Selection:</b>
<br />
<select class="uk-select uk-width-auto" v-model="selectedInstance" @change="onChange($event)">
<option v-bind:key="instance.name" v-for="instance in instances" v-bind:value="instance.apiurl">
{{ instance.name }}
</option>
</select>
</template>
<script>
import CountryMap from '@/utils/CountryMap.js'
export default {
data () {
return {
selectedInstance: null,
instances: [],
sponsorBlock: true,
skipSponsor: true,
skipIntro: false,
skipOutro: false,
skipPreview: false,
skipInteraction: true,
skipSelfPromo: true,
skipMusicOffTopic: true,
selectedTheme: 'dark',
autoPlayVideo: true,
listen: false,
resolutions: [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320],
defaultQuality: 0,
bufferingGoal: 10,
countryMap: CountryMap.COUNTRIES,
country: 'US',
defaultHomepage: 'trending',
showComments: true
}
},
mounted () {
if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} })
fetch('https://raw.githubusercontent.com/wiki/TeamPiped/Piped-Frontend/Instances.md')
.then(resp => resp.text())
.then(body => {
let skipped = 0
const lines = body.split('\n')
lines.forEach(line => {
const split = line.split('|')
if (split.length === 4) {
if (skipped < 2) {
skipped++
return
}
this.instances.push({
name: split[0].trim(),
apiurl: split[1].trim(),
locations: split[2].trim(),
cdn: split[3].trim()
})
}
})
})
if (localStorage) {
this.selectedInstance = this.getPreferenceString('instance', 'https://pipedapi.kavin.rocks')
this.sponsorBlock = this.getPreferenceBoolean('sponsorblock', true)
if (localStorage.getItem('selectedSkip') !== null) {
const skipList = localStorage.getItem('selectedSkip').split(',')
this.skipSponsor = this.skipIntro = this.skipOutro = this.skipPreview = this.skipInteraction = this.skipSelfPromo = this.skipMusicOffTopic = false
skipList.forEach(skip => {
switch (skip) {
case 'sponsor':
this.skipSponsor = true
break
case 'intro':
this.skipIntro = true
break
case 'outro':
this.skipOutro = true
break
case 'preview':
this.skipPreview = true
break
case 'interaction':
this.skipInteraction = true
break
case 'selfpromo':
this.skipSelfPromo = true
break
case 'music_offtopic':
this.skipMusicOffTopic = true
break
default:
console.log('Unknown sponsor type: ' + skip)
break
}
})
}
this.selectedTheme = this.getPreferenceString('theme', 'dark')
this.autoPlayVideo = this.getPreferenceBoolean(localStorage.getItem('playerAutoPlay'), true)
this.listen = this.getPreferenceBoolean('listen', false)
this.defaultQuality = Number(localStorage.getItem('quality'))
this.bufferingGoal = Math.max(Number(localStorage.getItem('bufferGoal')), 10)
this.country = this.getPreferenceString('region', 'US')
this.defaultHomepage = this.getPreferenceString('homepage', 'trending')
this.showComments = this.getPreferenceBoolean('comments', true)
}
},
methods: {
onChange () {
if (localStorage) {
let shouldReload = false
if (this.getPreferenceString('theme', 'dark') !== this.selectedTheme) shouldReload = true
localStorage.setItem('instance', this.selectedInstance)
localStorage.setItem('sponsorblock', this.sponsorBlock)
const sponsorSelected = []
if (this.skipSponsor) sponsorSelected.push('sponsor')
if (this.skipIntro) sponsorSelected.push('intro')
if (this.skipOutro) sponsorSelected.push('outro')
if (this.skipPreview) sponsorSelected.push('preview')
if (this.skipInteraction) sponsorSelected.push('interaction')
if (this.skipSelfPromo) sponsorSelected.push('selfpromo')
if (this.skipMusicOffTopic) sponsorSelected.push('music_offtopic')
localStorage.setItem('selectedSkip', sponsorSelected)
localStorage.setItem('theme', this.selectedTheme)
localStorage.setItem('playerAutoPlay', this.autoPlayVideo)
localStorage.setItem('listen', this.listen)
localStorage.setItem('quality', this.defaultQuality)
localStorage.setItem('bufferGoal', this.bufferingGoal)
localStorage.setItem('region', this.country)
localStorage.setItem('homepage', this.defaultHomepage)
localStorage.setItem('comments', this.showComments)
if (shouldReload) window.location.reload()
}
},
sslScore (url) {
return 'https://www.ssllabs.com/ssltest/analyze.html?d=' + new URL(url).host + '&latest'
}
}
}
</script>
-66
View File
@@ -1,66 +0,0 @@
<template>
<div class="uk-vertical-align uk-text-center uk-height-1-1 ">
<form class="uk-panel uk-panel-box">
<div class="uk-form-row">
<input
class="uk-width-1-1 uk-form-large uk-input uk-width-auto"
type="text"
v-model="username"
autocomplete="username"
placeholder="Username"
/>
</div>
<div class="uk-form-row">
<input
class="uk-width-1-1 uk-form-large uk-input uk-width-auto"
type="password"
v-model="password"
autocomplete="password"
placeholder="Password"
/>
</div>
<div class="uk-form-row">
<a
class="uk-width-1-1 uk-button uk-button-primary uk-button-large uk-width-auto"
style="background: #222"
@click="register"
>Register</a
>
</div>
</form>
</div>
</template>
<script>
export default {
data () {
return {
username: null,
password: null
}
},
mounted () {
// TODO: Add Server Side check
if (this.getAuthToken()) {
this.$router.push('/')
}
},
methods: {
register () {
console.log('authToken' + this.hashCode(this.apiUrl()))
this.fetchJson(this.apiUrl() + '/register', null, {
method: 'POST',
body: JSON.stringify({
username: this.username,
password: this.password
})
}).then(resp => {
if (resp.token) {
this.setPreference('authToken' + this.hashCode(this.apiUrl()), resp.token)
window.location = '/' // done to bypass cache
} else alert(resp.error)
})
}
}
}
</script>
-125
View File
@@ -1,125 +0,0 @@
<template>
<h1 class="uk-text-center">
{{ $route.query.search_query }}
</h1>
<b>Filter: </b>
<select
default="all"
class="uk-select uk-width-auto"
style="height: 100%"
v-model="selectedFilter"
@change="updateResults()"
>
<option v-bind:key="filter" v-for="filter in availableFilters" v-bind:value="filter">
{{ filter.replace("_", " ") }}
</option>
</select>
<hr />
<div v-if="results" class="uk-grid-xl" uk-grid="parallax: 0">
<div
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
v-bind:key="result.url"
v-for="result in results.items"
>
<div class="uk-text-secondary">
<router-link class="uk-text-emphasis" v-bind:to="result.url">
<img style="width: 100%" v-bind:src="result.thumbnail" loading="lazy" />
<p>
{{ result.name }}&thinsp;<font-awesome-icon
v-if="result.verified"
icon="check"
></font-awesome-icon>
</p>
</router-link>
<p v-if="result.description">{{ result.description }}</p>
<router-link class="uk-link-muted" v-if="result.uploaderUrl" v-bind:to="result.uploaderUrl">
<p>
{{ result.uploader }}&thinsp;<font-awesome-icon
v-if="result.uploaderVerified"
icon="check"
></font-awesome-icon>
</p>
</router-link>
<b v-if="result.duration" class="uk-text-small uk-align-right uk-text-align-right">
{{ timeFormat(result.duration) }}
</b>
<b v-if="result.uploadDate">
{{ result.uploadDate }}
</b>
<a v-if="result.uploaderName" class="uk-text-muted">{{ result.uploaderName }}</a>
<b v-if="result.videos >= 0"><br v-if="result.uploaderName" />{{ result.videos }} Videos</b>
<br />
<b v-if="result.views >= 0" class="uk-text-small">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ numberFormat(result.views) }} views
</b>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
results: null,
availableFilters: [
'all',
'videos',
'channels',
'playlists',
'music_songs',
'music_videos',
'music_albums',
'music_playlists'
],
selectedFilter: 'all'
}
},
mounted () {
this.updateResults()
},
activated () {
window.addEventListener('scroll', this.handleScroll)
},
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
},
methods: {
async fetchResults () {
return await await this.fetchJson(this.apiUrl() + '/search', {
q: this.$route.query.search_query,
filter: this.selectedFilter
})
},
async updateResults () {
document.title = this.$route.query.search_query + ' - Piped'
this.results = this.fetchResults().then(json => (this.results = json))
},
handleScroll () {
if (this.loading || !this.results || !this.results.nextpage) return
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - window.innerHeight) {
this.loading = true
this.fetchJson(this.apiUrl() + '/nextpage/search', {
nextpage: this.results.nextpage,
q: this.$route.query.search_query,
filter: this.selectedFilter
}).then(json => {
this.results.nextpage = json.nextpage
this.results.id = json.id
this.loading = false
json.items.map(stream => this.results.items.push(stream))
})
}
}
}
}
</script>
-102
View File
@@ -1,102 +0,0 @@
<template>
<div
class="uk-position-absolute uk-panel uk-box-shadow-large suggestions-container"
:style="[{ background: secondaryBackgroundColor }]"
>
<ul class="uk-list uk-margin-remove uk-text-secondary">
<li
v-for="(suggestion, i) in searchSuggestions"
:key="i"
:style="[selected === i ? { background: secondaryForegroundColor } : {}]"
@mouseover="onMouseOver(i)"
@mousedown.stop="onClick(i)"
class="uk-margin-remove suggestion"
>
{{ suggestion }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
searchText: String
},
data () {
return {
selected: 0,
searchSuggestions: []
}
},
methods: {
onKeyUp (e) {
if (e.key === 'ArrowUp') {
if (this.selected <= 0) {
this.setSelected(this.searchSuggestions.length - 1)
} else {
this.setSelected(this.selected - 1)
}
e.preventDefault()
} else if (e.key === 'ArrowDown') {
if (this.selected >= this.searchSuggestions.length - 1) {
this.setSelected(0)
} else {
this.setSelected(this.selected + 1)
}
e.preventDefault()
} else {
this.refreshSuggestions()
}
},
async refreshSuggestions () {
this.searchSuggestions = await this.fetchJson(this.apiUrl() + '/suggestions', {
query: this.searchText
})
this.searchSuggestions.unshift(this.searchText)
this.setSelected(0)
},
onMouseOver (i) {
if (i !== this.selected) {
this.selected = i
}
},
onClick (i) {
this.setSelected(i)
this.$router.push({
name: 'SearchResults',
query: { search_query: this.searchSuggestions[i] }
})
},
setSelected (val) {
this.selected = val
this.$emit('searchchange', this.searchSuggestions[this.selected])
}
}
}
</script>
<style>
.suggestions-container {
left: 50%;
transform: translateX(-50%);
max-width: 640px;
width: 100%;
box-sizing: border-box;
padding: 5px 0;
z-index: 10;
}
.suggestion {
padding: 4px 15px;
}
@media screen and (max-width: 959px) {
.suggestions-container {
max-width: calc(100% - 60px);
}
}
@media screen and (max-width: 639px) {
.suggestions-container {
max-width: calc(100% - 30px);
}
}
</style>
-45
View File
@@ -1,45 +0,0 @@
<template>
<h1 class="uk-text-bold uk-text-center">Trending</h1>
<hr />
<div class="uk-grid-xl" uk-grid="parallax: 0">
<div
:style="[{ background: backgroundColor }]"
class="uk-width-1-2 uk-width-1-3@s uk-width-1-4@m uk-width-1-5@l uk-width-1-6@xl"
v-bind:key="video.url"
v-for="video in videos"
>
<VideoItem :video="video" height="118" width="210" />
</div>
</div>
</template>
<script>
import VideoItem from '@/components/VideoItem.vue'
export default {
data () {
return {
videos: []
}
},
mounted () {
document.title = 'Trending - Piped'
const region = this.getPreferenceString('region', 'US')
this.fetchTrending(region).then(videos => (this.videos = videos))
},
methods: {
async fetchTrending (region) {
return await this.fetchJson(this.apiUrl() + '/trending', {
region: region || 'US'
})
}
},
components: {
VideoItem
}
}
</script>
-55
View File
@@ -1,55 +0,0 @@
<template>
<div class="uk-text-secondary" :style="[{ background: backgroundColor }]">
<router-link class="uk-text-emphasis" v-bind:to="video.url">
<img
:height="height"
:width="width"
style="width: 100%"
v-bind:src="video.thumbnail"
alt="thumbnail"
loading="lazy"
/>
<p>{{ video.title }}</p>
</router-link>
<div :class="{ 'uk-align-left': !(video.views >= 0 || video.uploadedDate) }">
<div v-if="video.uploaderUrl && video.uploaderName && !hideChannel">
<router-link class="uk-link-muted" :to="video.uploaderUrl">
<a>{{ video.uploaderName }}</a>
</router-link>
<br />
</div>
</div>
<b v-if="video.views >= 0 || video.uploadedDate" class="uk-text-small uk-align-left">
<div v-if="video.views >= 0">
<font-awesome-icon icon="eye"></font-awesome-icon>
{{ numberFormat(video.views) }} views
<br />
</div>
<div v-if="video.uploadedDate">
{{ video.uploadedDate }}
</div>
</b>
<div class="uk-align-right">
<b class="uk-text-small">{{ timeFormat(video.duration) }}</b>
<br />
<router-link :to="video.url + '&listen=1'">
<font-awesome-icon icon="headphones"></font-awesome-icon>
</router-link>
</div>
</div>
</template>
<script>
export default {
props: {
video: Object,
height: String,
width: String,
hideChannel: Boolean
}
}
</script>
-17
View File
@@ -1,17 +0,0 @@
<template>
<div>Loading...</div>
</template>
<script>
export default {
activated () {
const videoId = this.$route.params.videoId
if (videoId) {
this.$router.push({
path: '/watch',
query: { v: videoId }
})
}
}
}
</script>
-270
View File
@@ -1,270 +0,0 @@
<template>
<div class="uk-container uk-container-xlarge" v-if="video">
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
<div v-show="!video.error">
<Player
ref="videoPlayer"
:video="video"
:sponsors="sponsors"
:selectedAutoPlay="selectedAutoPlay"
:selectedAutoLoop="selectedAutoLoop"
/>
<div class="uk-text-bold uk-margin-small-top uk-text-large uk-text-emphasis">{{ video.title }}</div>
<div class="uk-flex uk-flex-middle">
<div class="uk-margin-small-right">{{ addCommas(video.views) }} views</div>
<div class="uk-margin-small-right">{{ video.uploadDate }}</div>
<div class="uk-flex-1"></div>
<div class="uk-margin-small-left">
<font-awesome-icon class="uk-margin-small-right" icon="thumbs-up"></font-awesome-icon>
<b>{{ addCommas(video.likes) }}</b>
</div>
<div class="uk-margin-small-left">
<font-awesome-icon class="uk-margin-small-right" icon="thumbs-down"></font-awesome-icon>
<b>{{ addCommas(video.dislikes) }}</b>
</div>
<a
:href="'https://youtu.be/' + getVideoId()"
class="uk-margin-small-left uk-button uk-button-small"
style="background: #222"
>
<font-awesome-icon class="uk-margin-small-right" :icon="['fab', 'youtube']"></font-awesome-icon>
<b>Watch on</b>
</a>
</div>
<div class="uk-flex uk-flex-middle uk-margin-small-top">
<img :src="video.uploaderAvatar" loading="lazy" />
<router-link class="uk-text-bold uk-margin-small-left" v-if="video.uploaderUrl" :to="video.uploaderUrl">
<a>{{ video.uploader }}</a>
</router-link>
<div class="uk-flex-1"></div>
<button
v-if="authenticated"
@click="subscribeHandler"
class="uk-button uk-button-small"
style="background: #222"
type="button"
>
{{ subscribed ? "Unsubscribe" : "Subscribe" }}
</button>
</div>
<hr />
<a class="uk-button uk-button-small" style="background: #222" @click="showDesc = !showDesc">
{{ showDesc ? "Minimize Description" : "Show Description" }}
</a>
<p v-show="showDesc" :style="[{ colour: foregroundColor }]" v-html="video.description"></p>
<div v-if="showDesc && sponsors && sponsors.segments">
Sponsors Segments: {{ sponsors.segments.length }}
</div>
</div>
<hr />
<b>Loop this Video:</b>&nbsp;
<input class="uk-checkbox" v-model="selectedAutoLoop" @change="onChange($event)" type="checkbox" />
<br />
<b>Auto Play next Video:</b>&nbsp;
<input class="uk-checkbox" v-model="selectedAutoPlay" @change="onChange($event)" type="checkbox" />
<hr />
<div uk-grid>
<div class="uk-width-4-5@xl uk-width-3-4@l uk-width-1" v-if="comments" ref="comments">
<div
class="uk-tile-default uk-align-left uk-width-expand"
:style="[{ background: backgroundColor }]"
v-bind:key="comment.commentId"
v-for="comment in comments.comments"
>
<div align="left">
<div v-if="comment.pinned">
<font-awesome-icon icon="thumbtack"></font-awesome-icon>&nbsp; Pinned by
{{ video.uploader }}
</div>
<img
:src="comment.thumbnail"
style="width: 10vmin"
height="176"
width="176"
loading="lazy"
alt="avatar"
/>
<br />
<router-link class="uk-link-muted" v-bind:to="comment.commentorUrl">
{{ comment.author }} </router-link
>&thinsp;<font-awesome-icon v-if="comment.verified" icon="check"></font-awesome-icon>
</div>
<p style="white-space: pre-wrap">{{ comment.commentText }}</p>
<div>
<b>{{ numberFormat(comment.likeCount) }}</b>
&nbsp;
<font-awesome-icon icon="thumbs-up"></font-awesome-icon>
&nbsp;
<font-awesome-icon v-if="comment.hearted" icon="heart"></font-awesome-icon>
</div>
<hr />
</div>
</div>
<div class="uk-width-1-5@xl uk-width-1-4@l uk-width-1 uk-flex-last@l uk-flex-first" v-if="video">
<div
class="uk-tile-default uk-width-auto"
:style="[{ background: backgroundColor }]"
v-bind:key="related.url"
v-for="related in video.relatedStreams"
>
<VideoItem :video="related" height="94" width="168" />
</div>
</div>
</div>
</div>
</template>
<script>
import Player from '@/components/Player.vue'
import VideoItem from '@/components/VideoItem.vue'
import ErrorHandler from '@/components/ErrorHandler.vue'
export default {
name: 'App',
data () {
return {
video: {
title: 'Loading...'
},
sponsors: null,
selectedAutoLoop: false,
selectedAutoPlay: null,
showDesc: true,
comments: null,
subscribed: false,
channelId: null
}
},
mounted () {
this.getVideoData().then(() => {
this.$refs.videoPlayer.loadVideo()
})
this.getSponsors()
if (this.getPreferenceBoolean('comments', true)) this.getComments()
},
activated () {
this.selectedAutoPlay = this.getPreferenceBoolean('autoplay', true)
if (this.video.duration) this.$refs.videoPlayer.loadVideo()
window.addEventListener('scroll', this.handleScroll)
},
deactivated () {
window.removeEventListener('scroll', this.handleScroll)
},
watch: {
'$route.query.v': function (v) {
if (v) {
window.scrollTo(0, 0)
}
}
},
methods: {
fetchVideo () {
return this.fetchJson(this.apiUrl() + '/streams/' + this.getVideoId())
},
async fetchSponsors () {
return await this.fetchJson(this.apiUrl() + '/sponsors/' + this.getVideoId(), {
category:
'["' +
this.getPreferenceString('selectedSkip', 'sponsor,interaction,selfpromo,music_offtopic').replaceAll(
',',
'","'
) +
'"]'
})
},
fetchComments () {
return this.fetchJson(this.apiUrl() + '/comments/' + this.getVideoId())
},
onChange () {
this.setPreference('autoplay', this.selectedAutoPlay)
},
async getVideoData () {
await this.fetchVideo()
.then(data => {
this.video = data
})
.then(() => {
if (!this.video.error) {
document.title = this.video.title + ' - Piped'
this.channelId = this.video.uploaderUrl.split('/')[2]
this.fetchSubscribedStatus()
this.video.description = this.purifyHTML(
this.video.description
.replaceAll('http://www.youtube.com', '')
.replaceAll('https://www.youtube.com', '')
.replaceAll('\n', '<br>')
)
}
})
},
async getSponsors () {
if (this.getPreferenceBoolean('sponsorblock', true)) { this.fetchSponsors().then(data => (this.sponsors = data)) }
},
async getComments () {
this.fetchComments().then(data => (this.comments = data))
},
async fetchSubscribedStatus () {
if (!this.channelId) return
this.fetchJson(
this.apiUrl() + '/subscribed',
{
channelId: this.channelId
},
{
headers: {
Authorization: this.getAuthToken()
}
}
).then(json => {
this.subscribed = json.subscribed
})
},
subscribeHandler () {
this.fetchJson(this.apiUrl() + (this.subscribed ? '/unsubscribe' : '/subscribe'), null, {
method: 'POST',
body: JSON.stringify({
channelId: this.channelId
}),
headers: {
Authorization: this.getAuthToken(),
'Content-Type': 'application/json'
}
})
this.subscribed = !this.subscribed
},
handleScroll () {
if (this.loading || !this.comments || !this.comments.nextpage) return
if (window.innerHeight + window.scrollY >= this.$refs.comments.offsetHeight - window.innerHeight) {
this.loading = true
this.fetchJson(this.apiUrl() + '/nextpage/comments/' + this.getVideoId(), {
url: this.comments.nextpage
}).then(json => {
this.comments.nextpage = json.nextpage
this.loading = false
json.comments.map(comment => this.comments.comments.push(comment))
})
}
},
getVideoId () {
return this.$route.query.v || this.$route.params.v
}
},
components: {
Player,
VideoItem,
ErrorHandler
}
}
</script>
+7 -183
View File
@@ -1,186 +1,10 @@
import { createApp } from 'vue'
import { keys as _keys } from 'lodash-es'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faThumbsUp,
faThumbsDown,
faEye,
faThumbtack,
faCheck,
faHeart,
faHeadphones,
faRss
} from '@fortawesome/free-solid-svg-icons'
import { faGithub, faBitcoin, faYoutube } from '@fortawesome/free-brands-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import router from '@/router/router'
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify'
import DOMPurify from 'dompurify'
Vue.config.productionTip = false
import TimeAgo from 'javascript-time-ago'
import en from 'javascript-time-ago/locale/en'
library.add(
faThumbsUp,
faThumbsDown,
faEye,
faGithub,
faBitcoin,
faThumbtack,
faCheck,
faHeart,
faHeadphones,
faYoutube,
faRss
)
TimeAgo.addDefaultLocale(en)
const timeAgo = new TimeAgo('en-US')
import('./registerServiceWorker')
const mixin = {
methods: {
timeFormat: function (duration) {
const pad = function (num, size) {
return ('000' + num).slice(size * -1)
}
const time = parseFloat(duration).toFixed(3)
const hours = Math.floor(time / 60 / 60)
const minutes = Math.floor(time / 60) % 60
const seconds = Math.floor(time - minutes * 60)
let str = ''
if (hours > 0) str += hours + ':'
str += pad(minutes, 2) + ':' + pad(seconds, 2)
return str
},
numberFormat (num) {
const digits = 2
const si = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'K' },
{ value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'B' }
]
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/
let i
for (i = si.length - 1; i > 0; i--) {
if (num >= si[i].value) {
break
}
}
return (num / si[i].value).toFixed(digits).replace(rx, '$1') + si[i].symbol
},
addCommas (num) {
num = parseInt(num)
return num.toLocaleString('en-US')
},
fetchJson: function (url, params, options) {
if (params) {
url = new URL(url)
for (const param of _keys(params)) url.searchParams.set(param, params[param])
}
return fetch(url, options).then(response => {
return response.json()
})
},
purifyHTML (original) {
return DOMPurify.sanitize(original)
},
setPreference (key, value) {
if (localStorage) localStorage.setItem(key, value)
},
getPreferenceBoolean (key, defaultVal) {
let value
if (
(value = this.$route.query[key]) !== undefined ||
(localStorage && (value = localStorage.getItem(key)) !== null)
) {
switch (String(value)) {
case 'true':
case '1':
case 'on':
case 'yes':
return true
default:
return false
}
} else return defaultVal
},
getPreferenceString (key, defaultVal) {
let value
if (
(value = this.$route.query[key]) !== undefined ||
(localStorage && (value = localStorage.getItem(key)) !== null)
) {
return value
} else return defaultVal
},
getPreferenceNumber (key, defaultVal) {
let value
if (
(value = this.$route.query[key]) !== undefined ||
(localStorage && (value = localStorage.getItem(key)) !== null)
) {
return Number(value)
} else return defaultVal
},
apiUrl () {
return this.getPreferenceString('instance', 'https://pipedapi.kavin.rocks')
},
getEffectiveTheme () {
let theme = this.getPreferenceString('theme', 'dark')
if (theme === 'auto') {
theme =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
return theme
},
getAuthToken () {
return this.getPreferenceString('authToken' + this.hashCode(this.apiUrl()))
},
hashCode (s) {
return s.split('').reduce(function (a, b) {
a = (a << 5) - a + b.charCodeAt(0)
return a & a
}, 0)
},
timeAgo (time) {
return timeAgo.format(time)
}
},
computed: {
backgroundColor () {
return this.getEffectiveTheme() === 'light' ? '#fff' : '#0b0e0f'
},
secondaryBackgroundColor () {
return this.getEffectiveTheme() === 'light' ? '#e5e5e5' : '#242727'
},
foregroundColor () {
return this.getEffectiveTheme() === 'light' ? '#15191a' : '#0b0e0f'
},
secondaryForegroundColor () {
return this.getEffectiveTheme() === 'light' ? '#666' : '#393d3d'
},
darkMode () {
return this.getEffectiveTheme() !== 'light'
},
authenticated (_this) {
return _this.getAuthToken() !== undefined
}
}
}
const app = createApp(App)
app.use(router)
app.mixin(mixin)
app.component('font-awesome-icon', FontAwesomeIcon)
app.mount('#app')
new Vue({
vuetify,
render: h => h(App)
}).$mount('#app')
+7
View File
@@ -0,0 +1,7 @@
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
Vue.use(Vuetify)
export default new Vuetify({
})
-33
View File
@@ -1,33 +0,0 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`/service-worker.js`, {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB",
);
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
window.location.reload();
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
}
-68
View File
@@ -1,68 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Trending',
component: () => import('@/components/TrendingPage')
},
{
path: '/preferences',
name: 'Preferences',
component: () => import('@/components/Preferences')
},
{
path: '/results',
name: 'SearchResults',
component: () => import('@/components/SearchResults')
},
{
path: '/playlist',
name: 'Playlist',
component: () => import('@/components/Playlist')
},
{
path: '/:path(v|w|embed|shorts|watch)/:v?',
name: 'WatchVideo',
component: () => import('@/components/WatchVideo')
},
{
path: '/:path(channel|user|c)/:channelId/:videos?',
name: 'Channel',
component: () => import('@/components/Channel')
},
{
path: '/login',
name: 'Login',
component: () => import('@/components/LoginPage')
},
{
path: '/register',
name: 'Register',
component: () => import('@/components/RegisterPage')
},
{
path: '/feed',
name: 'Feed',
component: () => import('@/components/FeedPage')
},
{
path: '/import',
name: 'Import',
component: () => import('@/components/ImportPage')
},
{
path: '/:videoId([a-zA-Z0-9_-]{11})',
component: () => import('@/components/VideoRedirect')
}
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior: function (_to, _from, savedPosition) {
return savedPosition || window.scrollTo(0, 0)
}
})
export default router
-253
View File
@@ -1,253 +0,0 @@
export default {
COUNTRIES: [
{ code: 'AD', name: 'Andorra' },
{ code: 'AE', name: 'United Arab Emirates' },
{ code: 'AF', name: 'Afghanistan' },
{ code: 'AG', name: 'Antigua And Barbuda' },
{ code: 'AI', name: 'Anguilla' },
{ code: 'AL', name: 'Albania' },
{ code: 'AM', name: 'Armenia' },
{ code: 'AO', name: 'Angola' },
{ code: 'AQ', name: 'Antarctica' },
{ code: 'AR', name: 'Argentina' },
{ code: 'AS', name: 'American Samoa' },
{ code: 'AT', name: 'Austria' },
{ code: 'AU', name: 'Australia' },
{ code: 'AW', name: 'Aruba' },
{ code: 'AX', name: 'Aland Islands' },
{ code: 'AZ', name: 'Azerbaijan' },
{ code: 'BA', name: 'Bosnia And Herzegovina' },
{ code: 'BB', name: 'Barbados' },
{ code: 'BD', name: 'Bangladesh' },
{ code: 'BE', name: 'Belgium' },
{ code: 'BF', name: 'Burkina Faso' },
{ code: 'BG', name: 'Bulgaria' },
{ code: 'BH', name: 'Bahrain' },
{ code: 'BI', name: 'Burundi' },
{ code: 'BJ', name: 'Benin' },
{ code: 'BL', name: 'Saint Barthelemy' },
{ code: 'BM', name: 'Bermuda' },
{ code: 'BN', name: 'Brunei Darussalam' },
{ code: 'BO', name: 'Bolivia' },
{ code: 'BQ', name: 'BQ' },
{ code: 'BR', name: 'Brazil' },
{ code: 'BS', name: 'Bahamas' },
{ code: 'BT', name: 'Bhutan' },
{ code: 'BV', name: 'Bouvet Island' },
{ code: 'BW', name: 'Botswana' },
{ code: 'BY', name: 'Belarus' },
{ code: 'BZ', name: 'Belize' },
{ code: 'CA', name: 'Canada' },
{ code: 'CC', name: 'Cocos (Keeling) Islands' },
{ code: 'CD', name: 'Congo, Democratic Republic' },
{ code: 'CF', name: 'Central African Republic' },
{ code: 'CG', name: 'Congo' },
{ code: 'CH', name: 'Switzerland' },
{ code: 'CI', name: "Cote D'Ivoire" },
{ code: 'CK', name: 'Cook Islands' },
{ code: 'CL', name: 'Chile' },
{ code: 'CM', name: 'Cameroon' },
{ code: 'CN', name: 'China' },
{ code: 'CO', name: 'Colombia' },
{ code: 'CR', name: 'Costa Rica' },
{ code: 'CU', name: 'Cuba' },
{ code: 'CV', name: 'Cape Verde' },
{ code: 'CW', name: 'CW' },
{ code: 'CX', name: 'Christmas Island' },
{ code: 'CY', name: 'Cyprus' },
{ code: 'CZ', name: 'Czech Republic' },
{ code: 'DE', name: 'Germany' },
{ code: 'DJ', name: 'Djibouti' },
{ code: 'DK', name: 'Denmark' },
{ code: 'DM', name: 'Dominica' },
{ code: 'DO', name: 'Dominican Republic' },
{ code: 'DZ', name: 'Algeria' },
{ code: 'EC', name: 'Ecuador' },
{ code: 'EE', name: 'Estonia' },
{ code: 'EG', name: 'Egypt' },
{ code: 'EH', name: 'Western Sahara' },
{ code: 'ER', name: 'Eritrea' },
{ code: 'ES', name: 'Spain' },
{ code: 'ET', name: 'Ethiopia' },
{ code: 'FI', name: 'Finland' },
{ code: 'FJ', name: 'Fiji' },
{ code: 'FK', name: 'Falkland Islands (Malvinas)' },
{ code: 'FM', name: 'Micronesia, Federated States Of' },
{ code: 'FO', name: 'Faroe Islands' },
{ code: 'FR', name: 'France' },
{ code: 'GA', name: 'Gabon' },
{ code: 'GB', name: 'United Kingdom' },
{ code: 'GD', name: 'Grenada' },
{ code: 'GE', name: 'Georgia' },
{ code: 'GF', name: 'French Guiana' },
{ code: 'GG', name: 'Guernsey' },
{ code: 'GH', name: 'Ghana' },
{ code: 'GI', name: 'Gibraltar' },
{ code: 'GL', name: 'Greenland' },
{ code: 'GM', name: 'Gambia' },
{ code: 'GN', name: 'Guinea' },
{ code: 'GP', name: 'Guadeloupe' },
{ code: 'GQ', name: 'Equatorial Guinea' },
{ code: 'GR', name: 'Greece' },
{ code: 'GS', name: 'South Georgia And Sandwich Isl.' },
{ code: 'GT', name: 'Guatemala' },
{ code: 'GU', name: 'Guam' },
{ code: 'GW', name: 'Guinea-Bissau' },
{ code: 'GY', name: 'Guyana' },
{ code: 'HK', name: 'Hong Kong' },
{ code: 'HM', name: 'Heard Island & Mcdonald Islands' },
{ code: 'HN', name: 'Honduras' },
{ code: 'HR', name: 'Croatia' },
{ code: 'HT', name: 'Haiti' },
{ code: 'HU', name: 'Hungary' },
{ code: 'ID', name: 'Indonesia' },
{ code: 'IE', name: 'Ireland' },
{ code: 'IL', name: 'Israel' },
{ code: 'IM', name: 'Isle Of Man' },
{ code: 'IN', name: 'India' },
{ code: 'IO', name: 'British Indian Ocean Territory' },
{ code: 'IQ', name: 'Iraq' },
{ code: 'IR', name: 'Iran, Islamic Republic Of' },
{ code: 'IS', name: 'Iceland' },
{ code: 'IT', name: 'Italy' },
{ code: 'JE', name: 'Jersey' },
{ code: 'JM', name: 'Jamaica' },
{ code: 'JO', name: 'Jordan' },
{ code: 'JP', name: 'Japan' },
{ code: 'KE', name: 'Kenya' },
{ code: 'KG', name: 'Kyrgyzstan' },
{ code: 'KH', name: 'Cambodia' },
{ code: 'KI', name: 'Kiribati' },
{ code: 'KM', name: 'Comoros' },
{ code: 'KN', name: 'Saint Kitts And Nevis' },
{ code: 'KP', name: 'KP' },
{ code: 'KR', name: 'Korea' },
{ code: 'KW', name: 'Kuwait' },
{ code: 'KY', name: 'Cayman Islands' },
{ code: 'KZ', name: 'Kazakhstan' },
{ code: 'LA', name: "Lao People's Democratic Republic" },
{ code: 'LB', name: 'Lebanon' },
{ code: 'LC', name: 'Saint Lucia' },
{ code: 'LI', name: 'Liechtenstein' },
{ code: 'LK', name: 'Sri Lanka' },
{ code: 'LR', name: 'Liberia' },
{ code: 'LS', name: 'Lesotho' },
{ code: 'LT', name: 'Lithuania' },
{ code: 'LU', name: 'Luxembourg' },
{ code: 'LV', name: 'Latvia' },
{ code: 'LY', name: 'Libyan Arab Jamahiriya' },
{ code: 'MA', name: 'Morocco' },
{ code: 'MC', name: 'Monaco' },
{ code: 'MD', name: 'Moldova' },
{ code: 'ME', name: 'Montenegro' },
{ code: 'MF', name: 'Saint Martin' },
{ code: 'MG', name: 'Madagascar' },
{ code: 'MH', name: 'Marshall Islands' },
{ code: 'MK', name: 'Macedonia' },
{ code: 'ML', name: 'Mali' },
{ code: 'MM', name: 'Myanmar' },
{ code: 'MN', name: 'Mongolia' },
{ code: 'MO', name: 'Macao' },
{ code: 'MP', name: 'Northern Mariana Islands' },
{ code: 'MQ', name: 'Martinique' },
{ code: 'MR', name: 'Mauritania' },
{ code: 'MS', name: 'Montserrat' },
{ code: 'MT', name: 'Malta' },
{ code: 'MU', name: 'Mauritius' },
{ code: 'MV', name: 'Maldives' },
{ code: 'MW', name: 'Malawi' },
{ code: 'MX', name: 'Mexico' },
{ code: 'MY', name: 'Malaysia' },
{ code: 'MZ', name: 'Mozambique' },
{ code: 'NA', name: 'Namibia' },
{ code: 'NC', name: 'New Caledonia' },
{ code: 'NE', name: 'Niger' },
{ code: 'NF', name: 'Norfolk Island' },
{ code: 'NG', name: 'Nigeria' },
{ code: 'NI', name: 'Nicaragua' },
{ code: 'NL', name: 'Netherlands' },
{ code: 'NO', name: 'Norway' },
{ code: 'NP', name: 'Nepal' },
{ code: 'NR', name: 'Nauru' },
{ code: 'NU', name: 'Niue' },
{ code: 'NZ', name: 'New Zealand' },
{ code: 'OM', name: 'Oman' },
{ code: 'PA', name: 'Panama' },
{ code: 'PE', name: 'Peru' },
{ code: 'PF', name: 'French Polynesia' },
{ code: 'PG', name: 'Papua New Guinea' },
{ code: 'PH', name: 'Philippines' },
{ code: 'PK', name: 'Pakistan' },
{ code: 'PL', name: 'Poland' },
{ code: 'PM', name: 'Saint Pierre And Miquelon' },
{ code: 'PN', name: 'Pitcairn' },
{ code: 'PR', name: 'Puerto Rico' },
{ code: 'PS', name: 'Palestinian Territory, Occupied' },
{ code: 'PT', name: 'Portugal' },
{ code: 'PW', name: 'Palau' },
{ code: 'PY', name: 'Paraguay' },
{ code: 'QA', name: 'Qatar' },
{ code: 'RE', name: 'Reunion' },
{ code: 'RO', name: 'Romania' },
{ code: 'RS', name: 'Serbia' },
{ code: 'RU', name: 'Russian Federation' },
{ code: 'RW', name: 'Rwanda' },
{ code: 'SA', name: 'Saudi Arabia' },
{ code: 'SB', name: 'Solomon Islands' },
{ code: 'SC', name: 'Seychelles' },
{ code: 'SD', name: 'Sudan' },
{ code: 'SE', name: 'Sweden' },
{ code: 'SG', name: 'Singapore' },
{ code: 'SH', name: 'Saint Helena' },
{ code: 'SI', name: 'Slovenia' },
{ code: 'SJ', name: 'Svalbard And Jan Mayen' },
{ code: 'SK', name: 'Slovakia' },
{ code: 'SL', name: 'Sierra Leone' },
{ code: 'SM', name: 'San Marino' },
{ code: 'SN', name: 'Senegal' },
{ code: 'SO', name: 'Somalia' },
{ code: 'SR', name: 'Suriname' },
{ code: 'SS', name: 'SS' },
{ code: 'ST', name: 'Sao Tome And Principe' },
{ code: 'SV', name: 'El Salvador' },
{ code: 'SX', name: 'SX' },
{ code: 'SY', name: 'Syrian Arab Republic' },
{ code: 'SZ', name: 'Swaziland' },
{ code: 'TC', name: 'Turks And Caicos Islands' },
{ code: 'TD', name: 'Chad' },
{ code: 'TF', name: 'French Southern Territories' },
{ code: 'TG', name: 'Togo' },
{ code: 'TH', name: 'Thailand' },
{ code: 'TJ', name: 'Tajikistan' },
{ code: 'TK', name: 'Tokelau' },
{ code: 'TL', name: 'Timor-Leste' },
{ code: 'TM', name: 'Turkmenistan' },
{ code: 'TN', name: 'Tunisia' },
{ code: 'TO', name: 'Tonga' },
{ code: 'TR', name: 'Turkey' },
{ code: 'TT', name: 'Trinidad And Tobago' },
{ code: 'TV', name: 'Tuvalu' },
{ code: 'TW', name: 'Taiwan' },
{ code: 'TZ', name: 'Tanzania' },
{ code: 'UA', name: 'Ukraine' },
{ code: 'UG', name: 'Uganda' },
{ code: 'UM', name: 'United States Outlying Islands' },
{ code: 'US', name: 'United States' },
{ code: 'UY', name: 'Uruguay' },
{ code: 'UZ', name: 'Uzbekistan' },
{ code: 'VA', name: 'Holy See (Vatican City State)' },
{ code: 'VC', name: 'Saint Vincent And Grenadines' },
{ code: 'VE', name: 'Venezuela' },
{ code: 'VG', name: 'Virgin Islands, British' },
{ code: 'VI', name: 'Virgin Islands, U.S.' },
{ code: 'VN', name: 'Viet Nam' },
{ code: 'VU', name: 'Vanuatu' },
{ code: 'WF', name: 'Wallis And Futuna' },
{ code: 'WS', name: 'Samoa' },
{ code: 'YE', name: 'Yemen' },
{ code: 'YT', name: 'Mayotte' },
{ code: 'ZA', name: 'South Africa' },
{ code: 'ZM', name: 'Zambia' },
{ code: 'ZW', name: 'Zimbabwe' }
]
}
-188
View File
@@ -1,188 +0,0 @@
// Based of https://github.com/GilgusMaximus/yt-dash-manifest-generator/blob/master/src/DashGenerator.js
const xml = require('xml-js')
const DashUtils = {
generate_dash_file_from_formats (VideoFormats, VideoLength) {
const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength)
return xml.json2xml(generatedJSON)
},
generate_xmljs_json_from_data (VideoFormatArray, VideoLength) {
const convertJSON = {
declaration: {
attributes: {
version: '1.0',
encoding: 'utf-8'
}
},
elements: [
{
type: 'element',
name: 'MPD',
attributes: {
xmlns: 'urn:mpeg:dash:schema:mpd:2011',
profiles: 'urn:mpeg:dash:profile:full:2011',
minBufferTime: 'PT1.5S',
type: 'static',
mediaPresentationDuration: `PT${VideoLength}S`
},
elements: [
{
type: 'element',
name: 'Period',
elements: this.generate_adaptation_set(VideoFormatArray)
}
]
}
]
}
return convertJSON
},
generate_adaptation_set (VideoFormatArray) {
const adaptationSets = []
const mimeTypes = []
const mimeObjects = [[]]
// sort the formats by mime types
VideoFormatArray.forEach(videoFormat => {
// the dual formats should not be used
if (videoFormat.mimeType.indexOf('video') != -1 && !videoFormat.videoOnly) {
return
}
// if these properties are not available, then we skip it because we cannot set these properties
// if (!(videoFormat.hasOwnProperty('initRange') && videoFormat.hasOwnProperty('indexRange'))) {
// return
// }
const mimeType = videoFormat.mimeType
const mimeTypeIndex = mimeTypes.indexOf(mimeType)
if (mimeTypeIndex > -1) {
mimeObjects[mimeTypeIndex].push(videoFormat)
} else {
mimeTypes.push(mimeType)
mimeObjects.push([])
mimeObjects[mimeTypes.length - 1].push(videoFormat)
}
})
// for each MimeType generate a new Adaptation set with Representations as sub elements
for (let i = 0; i < mimeTypes.length; i++) {
let isVideoFormat = false
const adapSet = {
type: 'element',
name: 'AdaptationSet',
attributes: {
id: i,
mimeType: mimeTypes[i],
startWithSAP: '1',
subsegmentAlignment: 'true'
},
elements: []
}
if (!mimeTypes[i].includes('audio')) {
adapSet.attributes.scanType = 'progressive'
isVideoFormat = true
}
mimeObjects[i].forEach(format => {
if (isVideoFormat) {
adapSet.elements.push(this.generate_representation_video(format))
} else {
adapSet.elements.push(this.generate_representation_audio(format))
}
})
adaptationSets.push(adapSet)
}
return adaptationSets
},
generate_representation_audio (Format) {
const representation = {
type: 'element',
name: 'Representation',
attributes: {
id: Format.itag,
codecs: Format.codec,
bandwidth: Format.bitrate
},
elements: [
{
type: 'element',
name: 'AudioChannelConfiguration',
attributes: {
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
value: '2'
}
},
{
type: 'element',
name: 'BaseURL',
elements: [
{
type: 'text',
text: Format.url
}
]
},
{
type: 'element',
name: 'SegmentBase',
attributes: {
indexRange: `${Format.indexStart}-${Format.indexEnd}`
},
elements: [
{
type: 'element',
name: 'Initialization',
attributes: {
range: `${Format.initStart}-${Format.initEnd}`
}
}
]
}
]
}
return representation
},
generate_representation_video (Format) {
const representation = {
type: 'element',
name: 'Representation',
attributes: {
id: Format.itag,
codecs: Format.codec,
bandwidth: Format.bitrate,
width: Format.width,
height: Format.height,
maxPlayoutRate: '1',
frameRate: Format.fps
},
elements: [
{
type: 'element',
name: 'BaseURL',
elements: [
{
type: 'text',
text: Format.url
}
]
},
{
type: 'element',
name: 'SegmentBase',
attributes: {
indexRange: `${Format.indexStart}-${Format.indexEnd}`
},
elements: [
{
type: 'element',
name: 'Initialization',
attributes: {
range: `${Format.initStart}-${Format.initEnd}`
}
}
]
}
]
}
return representation
}
}
export default DashUtils
+5 -17
View File
@@ -1,19 +1,7 @@
module.exports = {
pwa: {
name: 'Piped',
themeColor: '#fa4b4b',
msTileColor: '#000000',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
workboxPluginMode: 'GenerateSW',
workboxOptions: {
navigateFallback: 'index.html',
skipWaiting: true,
importWorkboxFrom: 'local',
runtimeCaching: [{
urlPattern: /\.(?:png|svg|ico)$/,
handler: 'CacheFirst'
}]
}
}
transpileDependencies: [
'vuetify'
],
lintOnSave: false
}
+1747 -1888
View File
File diff suppressed because it is too large Load Diff