mirror of
https://github.com/mmjee/Piped-Material.git
synced 2024-12-06 19:26:36 +01:00
Reformat
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
Generated
+2
-2
@@ -35,6 +35,7 @@
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="BLOCK_COMMENT_ADD_SPACE" value="true" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
@@ -45,8 +46,7 @@
|
||||
<option name="FOR_BRACE_FORCE" value="1" />
|
||||
<option name="SOFT_MARGINS" value="120" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
|
||||
+49
-1
@@ -70,7 +70,55 @@
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"no-tabs": "off",
|
||||
"indent": [
|
||||
"error",
|
||||
"tab",
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"VariableDeclarator": 1,
|
||||
"outerIIFEBody": 1,
|
||||
"MemberExpression": 1,
|
||||
"FunctionDeclaration": {
|
||||
"parameters": 1,
|
||||
"body": 1
|
||||
},
|
||||
"FunctionExpression": {
|
||||
"parameters": 1,
|
||||
"body": 1
|
||||
},
|
||||
"CallExpression": {
|
||||
"arguments": 1
|
||||
},
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1,
|
||||
"ImportDeclaration": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoreComments": false,
|
||||
"ignoredNodes": [
|
||||
"TemplateLiteral *",
|
||||
"JSXElement",
|
||||
"JSXElement > *",
|
||||
"JSXAttribute",
|
||||
"JSXIdentifier",
|
||||
"JSXNamespacedName",
|
||||
"JSXMemberExpression",
|
||||
"JSXSpreadAttribute",
|
||||
"JSXExpressionContainer",
|
||||
"JSXOpeningElement",
|
||||
"JSXClosingElement",
|
||||
"JSXFragment",
|
||||
"JSXOpeningFragment",
|
||||
"JSXClosingFragment",
|
||||
"JSXText",
|
||||
"JSXEmptyExpression",
|
||||
"JSXSpreadChild"
|
||||
],
|
||||
"offsetTernaryExpressions": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1% in US",
|
||||
|
||||
+90
-90
@@ -95,104 +95,104 @@ import { changeLocale } from '@/plugins/i18n'
|
||||
import AuthenticationModal from '@/components/AuthenticationModal'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AuthenticationModal,
|
||||
SearchMenu
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.homepage'),
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s - Piped'
|
||||
}
|
||||
},
|
||||
name: 'App',
|
||||
components: {
|
||||
AuthenticationModal,
|
||||
SearchMenu
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.homepage'),
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s - Piped'
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
languageOptions: [
|
||||
{ value: 'zh_Hant', text: 'Chinese (Traditional)' },
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'fr', text: 'French' },
|
||||
{ value: 'de', text: 'German' },
|
||||
{ value: 'el', text: 'Greek' },
|
||||
{ value: 'zh_Hans', text: 'Chinese (Simplified, only loads the font)' },
|
||||
{ value: 'jp', text: 'Japanese (only loads fonts)' },
|
||||
{ value: 'kr', text: 'Korean (only loads fonts)' },
|
||||
{ value: 'cyrl', text: 'Cyrillic Languages (fonts)' },
|
||||
{ value: 'lt', text: 'Lithuanian' },
|
||||
{ value: 'ml', text: 'Malayalam' },
|
||||
{ value: 'nb_NO', text: 'Norwegian Bokmål' },
|
||||
{ value: 'tr', text: 'Turkish' },
|
||||
{ value: 'bn_beng', text: 'Bengali (বাংলা)' }
|
||||
// Incomplete, DO NOT USE
|
||||
/* { value: 'bn_latn', text: 'Bengali (Latin)' }, */
|
||||
].sort((a, b) => {
|
||||
return a.text.localeCompare(b.text)
|
||||
}),
|
||||
drawer: false
|
||||
}),
|
||||
data: () => ({
|
||||
languageOptions: [
|
||||
{ value: 'zh_Hant', text: 'Chinese (Traditional)' },
|
||||
{ value: 'en', text: 'English' },
|
||||
{ value: 'fr', text: 'French' },
|
||||
{ value: 'de', text: 'German' },
|
||||
{ value: 'el', text: 'Greek' },
|
||||
{ value: 'zh_Hans', text: 'Chinese (Simplified, only loads the font)' },
|
||||
{ value: 'jp', text: 'Japanese (only loads fonts)' },
|
||||
{ value: 'kr', text: 'Korean (only loads fonts)' },
|
||||
{ value: 'cyrl', text: 'Cyrillic Languages (fonts)' },
|
||||
{ value: 'lt', text: 'Lithuanian' },
|
||||
{ value: 'ml', text: 'Malayalam' },
|
||||
{ value: 'nb_NO', text: 'Norwegian Bokmål' },
|
||||
{ value: 'tr', text: 'Turkish' },
|
||||
{ value: 'bn_beng', text: 'Bengali (বাংলা)' }
|
||||
// Incomplete, DO NOT USE
|
||||
/* { value: 'bn_latn', text: 'Bengali (Latin)' }, */
|
||||
].sort((a, b) => {
|
||||
return a.text.localeCompare(b.text)
|
||||
}),
|
||||
drawer: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
bgStyles () {
|
||||
return {
|
||||
backgroundColor: this.$vuetify.theme.dark ? '#282828' : '#fbf1c7'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bgStyles () {
|
||||
return {
|
||||
backgroundColor: this.$vuetify.theme.dark ? '#282828' : '#fbf1c7'
|
||||
}
|
||||
},
|
||||
|
||||
links () {
|
||||
const links = [
|
||||
{
|
||||
id: 'trending',
|
||||
name: 'titles.trending',
|
||||
to: '/trending'
|
||||
},
|
||||
{
|
||||
id: 'prefs',
|
||||
name: 'titles.preferences',
|
||||
to: '/preferences'
|
||||
},
|
||||
{
|
||||
id: 'watch-history',
|
||||
name: 'titles.history',
|
||||
to: '/watch-history'
|
||||
}
|
||||
]
|
||||
links () {
|
||||
const links = [
|
||||
{
|
||||
id: 'trending',
|
||||
name: 'titles.trending',
|
||||
to: '/trending'
|
||||
},
|
||||
{
|
||||
id: 'prefs',
|
||||
name: 'titles.preferences',
|
||||
to: '/preferences'
|
||||
},
|
||||
{
|
||||
id: 'watch-history',
|
||||
name: 'titles.history',
|
||||
to: '/watch-history'
|
||||
}
|
||||
]
|
||||
|
||||
if (this.$store.getters['auth/isCurrentlyAuthenticated']) {
|
||||
links.splice(0, 0, {
|
||||
id: 'feed',
|
||||
name: 'titles.feed',
|
||||
to: '/feed'
|
||||
})
|
||||
}
|
||||
if (this.$store.getters['auth/isCurrentlyAuthenticated']) {
|
||||
links.splice(0, 0, {
|
||||
id: 'feed',
|
||||
name: 'titles.feed',
|
||||
to: '/feed'
|
||||
})
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
},
|
||||
return links
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.prefs.prefs.darkMode' (newVal) {
|
||||
this.$vuetify.theme.dark = newVal
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$store.state.prefs.prefs.darkMode' (newVal) {
|
||||
this.$vuetify.theme.dark = newVal
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeLocale (lang) {
|
||||
return changeLocale(lang)
|
||||
},
|
||||
methods: {
|
||||
changeLocale (lang) {
|
||||
return changeLocale(lang)
|
||||
},
|
||||
|
||||
toggleDarkMode () {
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'darkMode',
|
||||
value: this.$vuetify.theme.dark
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleDarkMode () {
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'darkMode',
|
||||
value: this.$vuetify.theme.dark
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('prefs/loadState')
|
||||
this.$store.dispatch('auth/initializeAuth')
|
||||
}
|
||||
created () {
|
||||
this.$store.dispatch('prefs/loadState')
|
||||
this.$store.dispatch('auth/initializeAuth')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -47,50 +47,50 @@
|
||||
import { AuthenticationError } from '@/store/authentication-store'
|
||||
|
||||
export default {
|
||||
name: 'AuthenticationModal',
|
||||
props: ['listMode'],
|
||||
data: () => ({
|
||||
error: null,
|
||||
requestInProgress: false,
|
||||
dialogOpen: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}),
|
||||
methods: {
|
||||
async doCall (path) {
|
||||
this.error = null
|
||||
try {
|
||||
this.requestInProgress = true
|
||||
await this.$store.dispatch('auth/loginOrRegister', {
|
||||
path,
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
this.dialogOpen = false
|
||||
} catch (e) {
|
||||
if (!(e instanceof AuthenticationError)) {
|
||||
throw e
|
||||
}
|
||||
name: 'AuthenticationModal',
|
||||
props: ['listMode'],
|
||||
data: () => ({
|
||||
error: null,
|
||||
requestInProgress: false,
|
||||
dialogOpen: false,
|
||||
username: '',
|
||||
password: ''
|
||||
}),
|
||||
methods: {
|
||||
async doCall (path) {
|
||||
this.error = null
|
||||
try {
|
||||
this.requestInProgress = true
|
||||
await this.$store.dispatch('auth/loginOrRegister', {
|
||||
path,
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
this.dialogOpen = false
|
||||
} catch (e) {
|
||||
if (!(e instanceof AuthenticationError)) {
|
||||
throw e
|
||||
}
|
||||
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
this.error = e.message
|
||||
} finally {
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
logOut () {
|
||||
this.$store.commit('auth/deleteAuthToken', {
|
||||
apiURL: this.$store.getters['prefs/apiUrl']
|
||||
})
|
||||
},
|
||||
logOut () {
|
||||
this.$store.commit('auth/deleteAuthToken', {
|
||||
apiURL: this.$store.getters['prefs/apiUrl']
|
||||
})
|
||||
},
|
||||
|
||||
login () {
|
||||
return this.doCall('login')
|
||||
},
|
||||
login () {
|
||||
return this.doCall('login')
|
||||
},
|
||||
|
||||
register () {
|
||||
return this.doCall('register')
|
||||
}
|
||||
}
|
||||
register () {
|
||||
return this.doCall('register')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
error: String,
|
||||
message: String
|
||||
},
|
||||
data: () => ({
|
||||
showStacktrace: false
|
||||
})
|
||||
props: {
|
||||
error: String,
|
||||
message: String
|
||||
},
|
||||
data: () => ({
|
||||
showStacktrace: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
import { LibPiped } from '@/tools/libpiped'
|
||||
|
||||
export default {
|
||||
name: 'ExpandableDate',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
timeAgo () {
|
||||
return LibPiped.timeAgo(this.date)
|
||||
},
|
||||
name: 'ExpandableDate',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
timeAgo () {
|
||||
return LibPiped.timeAgo(this.date)
|
||||
},
|
||||
|
||||
formattedDate () {
|
||||
return LibPiped.formatFullDate(this.date)
|
||||
}
|
||||
}
|
||||
formattedDate () {
|
||||
return LibPiped.formatFullDate(this.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,16 +22,16 @@
|
||||
import { isNumber } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
item: Object,
|
||||
height: Number,
|
||||
width: Number,
|
||||
hideChannel: Boolean
|
||||
},
|
||||
methods: {
|
||||
isNumber (...args) {
|
||||
return isNumber(...args)
|
||||
}
|
||||
}
|
||||
props: {
|
||||
item: Object,
|
||||
height: Number,
|
||||
width: Number,
|
||||
hideChannel: Boolean
|
||||
},
|
||||
methods: {
|
||||
isNumber (...args) {
|
||||
return isNumber(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+271
-271
@@ -43,310 +43,310 @@ import { setupKeybindings } from '@/plugins/keybindings'
|
||||
window.muxjs = muxjs
|
||||
|
||||
export default {
|
||||
props: {
|
||||
video: Object,
|
||||
sponsors: Object,
|
||||
skipToTime: Number,
|
||||
selectedAutoLoop: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
$player: null,
|
||||
$ui: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldAutoPlay () {
|
||||
return this.$store.getters['prefs/getPreferenceBoolean']('playerAutoPlay', true)
|
||||
},
|
||||
props: {
|
||||
video: Object,
|
||||
sponsors: Object,
|
||||
skipToTime: Number,
|
||||
selectedAutoLoop: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
$player: null,
|
||||
$ui: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldAutoPlay () {
|
||||
return this.$store.getters['prefs/getPreferenceBoolean']('playerAutoPlay', true)
|
||||
},
|
||||
|
||||
preferredVideoCodecs () {
|
||||
const preferredVideoCodecs = []
|
||||
if (this.$refs.videoEl.canPlayType('video/mp4; codecs="av01.0.08M.08"') !== '') { preferredVideoCodecs.push('av01') }
|
||||
if (this.$refs.videoEl.canPlayType('video/webm; codecs="vp9"') !== '') preferredVideoCodecs.push('vp9')
|
||||
if (this.$refs.videoEl.canPlayType('video/mp4; codecs="avc1.4d401f"') !== '') { preferredVideoCodecs.push('avc1') }
|
||||
return preferredVideoCodecs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentTime () {
|
||||
// UNCLEAN
|
||||
return this.$refs.videoEl ? this.$refs.videoEl.currentTime : undefined
|
||||
},
|
||||
preferredVideoCodecs () {
|
||||
const preferredVideoCodecs = []
|
||||
if (this.$refs.videoEl.canPlayType('video/mp4; codecs="av01.0.08M.08"') !== '') { preferredVideoCodecs.push('av01') }
|
||||
if (this.$refs.videoEl.canPlayType('video/webm; codecs="vp9"') !== '') preferredVideoCodecs.push('vp9')
|
||||
if (this.$refs.videoEl.canPlayType('video/mp4; codecs="avc1.4d401f"') !== '') { preferredVideoCodecs.push('avc1') }
|
||||
return preferredVideoCodecs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentTime () {
|
||||
// UNCLEAN
|
||||
return this.$refs.videoEl ? this.$refs.videoEl.currentTime : undefined
|
||||
},
|
||||
|
||||
async loadVideo () {
|
||||
console.log('PIPED | LOADING VIDEO')
|
||||
const component = this
|
||||
const videoEl = this.$refs.videoEl
|
||||
async loadVideo () {
|
||||
console.log('PIPED | LOADING VIDEO')
|
||||
const component = this
|
||||
const videoEl = this.$refs.videoEl
|
||||
|
||||
videoEl.setAttribute('poster', this.video.thumbnailUrl)
|
||||
videoEl.setAttribute('poster', this.video.thumbnailUrl)
|
||||
|
||||
if (this.skipToTime != null) videoEl.currentTime = this.skipToTime
|
||||
if (this.skipToTime != null) videoEl.currentTime = this.skipToTime
|
||||
|
||||
const noPrevPlayer = !this.$player
|
||||
const noPrevPlayer = !this.$player
|
||||
|
||||
const streams = []
|
||||
const streams = []
|
||||
|
||||
streams.push(...this.video.audioStreams)
|
||||
streams.push(...this.video.videoStreams)
|
||||
streams.push(...this.video.audioStreams)
|
||||
streams.push(...this.video.videoStreams)
|
||||
|
||||
const MseSupport = window.MediaSource !== undefined
|
||||
const lbry = this.$store.getters['prefs/getPreferenceBoolean']('disableLBRY', true)
|
||||
? null
|
||||
: this.video.videoStreams.filter(stream => stream.quality === 'LBRY')[0]
|
||||
let uri, mime
|
||||
const MseSupport = window.MediaSource !== undefined
|
||||
const lbry = this.$store.getters['prefs/getPreferenceBoolean']('disableLBRY', true)
|
||||
? null
|
||||
: this.video.videoStreams.filter(stream => stream.quality === 'LBRY')[0]
|
||||
let uri, mime
|
||||
|
||||
if (this.video.livestream) {
|
||||
uri = this.video.hls
|
||||
mime = 'application/x-mpegURL'
|
||||
} else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
|
||||
if (!this.video.dash) {
|
||||
const dash = DashUtils.generate_dash_file_from_formats(
|
||||
streams,
|
||||
this.video.duration
|
||||
)
|
||||
uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash)
|
||||
} else {
|
||||
uri = this.video.dash
|
||||
}
|
||||
mime = 'application/dash+xml'
|
||||
} else if (lbry) {
|
||||
uri = lbry.url
|
||||
if (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true)) {
|
||||
const url = new URL(uri)
|
||||
url.searchParams.set('host', url.host)
|
||||
url.host = new URL(this.video.proxyUrl).host
|
||||
uri = url.toString()
|
||||
}
|
||||
mime = await fetch(uri, {
|
||||
method: 'HEAD'
|
||||
}).then(response => response.headers.get('Content-Type'))
|
||||
} else if (this.video.hls) {
|
||||
uri = this.video.hls
|
||||
mime = 'application/x-mpegURL'
|
||||
} else {
|
||||
uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url
|
||||
}
|
||||
if (this.video.livestream) {
|
||||
uri = this.video.hls
|
||||
mime = 'application/x-mpegURL'
|
||||
} else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
|
||||
if (!this.video.dash) {
|
||||
const dash = DashUtils.generate_dash_file_from_formats(
|
||||
streams,
|
||||
this.video.duration
|
||||
)
|
||||
uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash)
|
||||
} else {
|
||||
uri = this.video.dash
|
||||
}
|
||||
mime = 'application/dash+xml'
|
||||
} else if (lbry) {
|
||||
uri = lbry.url
|
||||
if (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true)) {
|
||||
const url = new URL(uri)
|
||||
url.searchParams.set('host', url.host)
|
||||
url.host = new URL(this.video.proxyUrl).host
|
||||
uri = url.toString()
|
||||
}
|
||||
mime = await fetch(uri, {
|
||||
method: 'HEAD'
|
||||
}).then(response => response.headers.get('Content-Type'))
|
||||
} else if (this.video.hls) {
|
||||
uri = this.video.hls
|
||||
mime = 'application/x-mpegURL'
|
||||
} else {
|
||||
uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url
|
||||
}
|
||||
|
||||
if (noPrevPlayer) {
|
||||
shaka.polyfill.installAll()
|
||||
if (noPrevPlayer) {
|
||||
shaka.polyfill.installAll()
|
||||
|
||||
const localPlayer = new shaka.Player(videoEl)
|
||||
const proxyHost = new URL(component.video.proxyUrl).host
|
||||
const localPlayer = new shaka.Player(videoEl)
|
||||
const proxyHost = new URL(component.video.proxyUrl).host
|
||||
|
||||
localPlayer.getNetworkingEngine().registerRequestFilter((_type, request) => {
|
||||
const uri = request.uris[0]
|
||||
const url = new URL(uri)
|
||||
const headers = request.headers
|
||||
if (
|
||||
url.host.endsWith('.googlevideo.com') ||
|
||||
localPlayer.getNetworkingEngine().registerRequestFilter((_type, request) => {
|
||||
const uri = request.uris[0]
|
||||
const url = new URL(uri)
|
||||
const headers = request.headers
|
||||
if (
|
||||
url.host.endsWith('.googlevideo.com') ||
|
||||
(url.host.endsWith('.lbryplayer.xyz') && (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true) || headers.Range))
|
||||
) {
|
||||
url.searchParams.set('host', url.host)
|
||||
url.host = proxyHost
|
||||
request.uris[0] = url.toString()
|
||||
}
|
||||
) {
|
||||
url.searchParams.set('host', url.host)
|
||||
url.host = proxyHost
|
||||
request.uris[0] = url.toString()
|
||||
}
|
||||
|
||||
if (url.pathname === '/videoplayback') {
|
||||
if (headers.Range) {
|
||||
url.searchParams.set('range', headers.Range.split('=')[1])
|
||||
request.headers = {}
|
||||
request.uris[0] = url.toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
if (url.pathname === '/videoplayback') {
|
||||
if (headers.Range) {
|
||||
url.searchParams.set('range', headers.Range.split('=')[1])
|
||||
request.headers = {}
|
||||
request.uris[0] = url.toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
localPlayer.configure(
|
||||
'streaming.bufferingGoal',
|
||||
this.$store.getters['prefs/getPreferenceNumber']('bufferGoal', 10)
|
||||
)
|
||||
localPlayer.configure(
|
||||
'streaming.bufferingGoal',
|
||||
this.$store.getters['prefs/getPreferenceNumber']('bufferGoal', 10)
|
||||
)
|
||||
|
||||
this.setPlayerAttrs(localPlayer, videoEl, uri, mime, shaka)
|
||||
} else this.setPlayerAttrs(this.$player, videoEl, uri, mime, shaka)
|
||||
this.setPlayerAttrs(localPlayer, videoEl, uri, mime, shaka)
|
||||
} else this.setPlayerAttrs(this.$player, videoEl, uri, mime, shaka)
|
||||
|
||||
if (noPrevPlayer) {
|
||||
videoEl.addEventListener('timeupdate', (ev) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.$emit('timeupdate', ev)
|
||||
})
|
||||
if (noPrevPlayer) {
|
||||
videoEl.addEventListener('timeupdate', (ev) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this.$emit('timeupdate', ev)
|
||||
})
|
||||
|
||||
videoEl.addEventListener('volumechange', () => {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'volume',
|
||||
value: videoEl.volume
|
||||
})
|
||||
})
|
||||
videoEl.addEventListener('volumechange', () => {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'volume',
|
||||
value: videoEl.volume
|
||||
})
|
||||
})
|
||||
|
||||
videoEl.addEventListener('ratechange', () => {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'rate',
|
||||
value: videoEl.playbackRate
|
||||
})
|
||||
})
|
||||
videoEl.addEventListener('ratechange', () => {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'rate',
|
||||
value: videoEl.playbackRate
|
||||
})
|
||||
})
|
||||
|
||||
videoEl.addEventListener('ended', () => {
|
||||
this.$emit('videoEnded')
|
||||
})
|
||||
}
|
||||
videoEl.addEventListener('ended', () => {
|
||||
this.$emit('videoEnded')
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
|
||||
},
|
||||
setPlayerAttrs (localPlayer, videoEl, uri, mime, shaka) {
|
||||
if (!this.$ui) {
|
||||
this.$ui = new shaka.ui.Overlay(localPlayer, this.$refs.container, videoEl)
|
||||
// TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
|
||||
},
|
||||
setPlayerAttrs (localPlayer, videoEl, uri, mime, 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)'
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
this.$ui.configure(config)
|
||||
}
|
||||
|
||||
const player = this.$ui.getControls().getPlayer()
|
||||
const player = this.$ui.getControls().getPlayer()
|
||||
|
||||
this.$player = player
|
||||
this.$player = player
|
||||
|
||||
const disableVideo = this.$store.getters['prefs/getPreferenceBoolean']('listen', false) && !this.video.livestream
|
||||
const disableVideo = this.$store.getters['prefs/getPreferenceBoolean']('listen', false) && !this.video.livestream
|
||||
|
||||
this.$player.configure({
|
||||
preferredVideoCodecs: this.preferredVideoCodecs,
|
||||
preferredAudioCodecs: ['opus', 'mp4a'],
|
||||
manifest: {
|
||||
disableVideo: disableVideo,
|
||||
hls: {
|
||||
useFullSegmentsForStartTime: true
|
||||
}
|
||||
},
|
||||
streaming: {
|
||||
useNativeHlsOnSafari: false
|
||||
}
|
||||
})
|
||||
this.$player.configure({
|
||||
preferredVideoCodecs: this.preferredVideoCodecs,
|
||||
preferredAudioCodecs: ['opus', 'mp4a'],
|
||||
manifest: {
|
||||
disableVideo: disableVideo,
|
||||
hls: {
|
||||
useFullSegmentsForStartTime: true
|
||||
}
|
||||
},
|
||||
streaming: {
|
||||
useNativeHlsOnSafari: false
|
||||
}
|
||||
})
|
||||
|
||||
const quality = this.$store.getters['prefs/getPreferenceNumber']('quality', 0)
|
||||
const qualityConds = quality > 0 && (this.video.audioStreams.length > 0 || this.video.livestream) && !disableVideo
|
||||
if (qualityConds) this.$player.configure('abr.enabled', false)
|
||||
const quality = this.$store.getters['prefs/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, mime).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)
|
||||
}
|
||||
player.load(uri, 0, mime).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.$store.getters['prefs/getPreferenceNumber']('volume', 1)
|
||||
player.trickPlay(this.$store.getters['prefs/getPreferenceNumber']('rate', 1))
|
||||
})
|
||||
}
|
||||
},
|
||||
this.video.subtitles.forEach(subtitle => {
|
||||
player.addTextTrackAsync(
|
||||
subtitle.url,
|
||||
subtitle.code,
|
||||
'SUBTITLE',
|
||||
subtitle.mimeType,
|
||||
null,
|
||||
subtitle.name
|
||||
)
|
||||
})
|
||||
videoEl.volume = this.$store.getters['prefs/getPreferenceNumber']('volume', 1)
|
||||
player.trickPlay(this.$store.getters['prefs/getPreferenceNumber']('rate', 1))
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'video.videoId' () {
|
||||
this.loadVideo()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'video.videoId' () {
|
||||
this.loadVideo()
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadVideo()
|
||||
const self = this
|
||||
const videoEl = self.$refs.videoEl
|
||||
const onSpace = (e) => {
|
||||
if (videoEl.paused) videoEl.play()
|
||||
else videoEl.pause()
|
||||
e.preventDefault()
|
||||
}
|
||||
mounted () {
|
||||
this.loadVideo()
|
||||
const self = this
|
||||
const videoEl = self.$refs.videoEl
|
||||
const onSpace = (e) => {
|
||||
if (videoEl.paused) videoEl.play()
|
||||
else videoEl.pause()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
this.unsubToKeybindings = setupKeybindings(window, {
|
||||
f (e) {
|
||||
if (document.fullscreenElement) document.exitFullscreen()
|
||||
else self.$refs.container.requestFullscreen()
|
||||
e.preventDefault()
|
||||
},
|
||||
m (e) {
|
||||
videoEl.muted = !videoEl.muted
|
||||
e.preventDefault()
|
||||
},
|
||||
j (e) {
|
||||
videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
l (e) {
|
||||
videoEl.currentTime = videoEl.currentTime + 15
|
||||
e.preventDefault()
|
||||
},
|
||||
c (e) {
|
||||
self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible())
|
||||
e.preventDefault()
|
||||
},
|
||||
Space: onSpace,
|
||||
k: onSpace,
|
||||
ArrowUp (e) {
|
||||
videoEl.volume = Math.min(videoEl.volume + 0.05, 1)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowDown (e) {
|
||||
videoEl.volume = Math.max(videoEl.volume - 0.05, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowLeft (e) {
|
||||
videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowRight (e) {
|
||||
videoEl.currentTime = videoEl.currentTime + 5
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
},
|
||||
this.unsubToKeybindings = setupKeybindings(window, {
|
||||
f (e) {
|
||||
if (document.fullscreenElement) document.exitFullscreen()
|
||||
else self.$refs.container.requestFullscreen()
|
||||
e.preventDefault()
|
||||
},
|
||||
m (e) {
|
||||
videoEl.muted = !videoEl.muted
|
||||
e.preventDefault()
|
||||
},
|
||||
j (e) {
|
||||
videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
l (e) {
|
||||
videoEl.currentTime = videoEl.currentTime + 15
|
||||
e.preventDefault()
|
||||
},
|
||||
c (e) {
|
||||
self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible())
|
||||
e.preventDefault()
|
||||
},
|
||||
Space: onSpace,
|
||||
k: onSpace,
|
||||
ArrowUp (e) {
|
||||
videoEl.volume = Math.min(videoEl.volume + 0.05, 1)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowDown (e) {
|
||||
videoEl.volume = Math.max(videoEl.volume - 0.05, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowLeft (e) {
|
||||
videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0)
|
||||
e.preventDefault()
|
||||
},
|
||||
ArrowRight (e) {
|
||||
videoEl.currentTime = videoEl.currentTime + 5
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
if (this.$ui) {
|
||||
this.$ui.destroy()
|
||||
this.$ui = undefined
|
||||
this.$player = undefined
|
||||
}
|
||||
if (this.$player) {
|
||||
this.$player.destroy()
|
||||
this.$player = undefined
|
||||
}
|
||||
this.unsubToKeybindings()
|
||||
this.$refs.container.querySelectorAll('div').forEach(node => node.remove())
|
||||
}
|
||||
beforeDestroy () {
|
||||
if (this.$ui) {
|
||||
this.$ui.destroy()
|
||||
this.$ui = undefined
|
||||
this.$player = undefined
|
||||
}
|
||||
if (this.$player) {
|
||||
this.$player.destroy()
|
||||
this.$player = undefined
|
||||
}
|
||||
this.unsubToKeybindings()
|
||||
this.$refs.container.querySelectorAll('div').forEach(node => node.remove())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,52 +9,52 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['channelId'],
|
||||
data: () => ({
|
||||
showButtons: false,
|
||||
subscribed: false
|
||||
}),
|
||||
props: ['channelId'],
|
||||
data: () => ({
|
||||
showButtons: false,
|
||||
subscribed: false
|
||||
}),
|
||||
|
||||
methods: {
|
||||
async checkStatus () {
|
||||
this.showButtons = false
|
||||
if (!(this.channelId && this.isAuthenticated)) {
|
||||
return
|
||||
}
|
||||
methods: {
|
||||
async checkStatus () {
|
||||
this.showButtons = false
|
||||
if (!(this.channelId && this.isAuthenticated)) {
|
||||
return
|
||||
}
|
||||
|
||||
const resp = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/subscribed',
|
||||
params: {
|
||||
channelId: this.channelId
|
||||
}
|
||||
})
|
||||
const resp = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/subscribed',
|
||||
params: {
|
||||
channelId: this.channelId
|
||||
}
|
||||
})
|
||||
|
||||
this.subscribed = resp.subscribed
|
||||
this.showButtons = true
|
||||
},
|
||||
this.subscribed = resp.subscribed
|
||||
this.showButtons = true
|
||||
},
|
||||
|
||||
async subscribeHandler () {
|
||||
await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'POST',
|
||||
path: (this.subscribed ? '/unsubscribe' : '/subscribe'),
|
||||
data: {
|
||||
channelId: this.channelId
|
||||
}
|
||||
})
|
||||
this.subscribed = !this.subscribed
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAuthenticated () {
|
||||
return this.$store.getters['auth/isCurrentlyAuthenticated']
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.checkStatus()
|
||||
},
|
||||
watch: {
|
||||
channelId: 'checkStatus',
|
||||
isAuthenticated: 'checkStatus'
|
||||
}
|
||||
async subscribeHandler () {
|
||||
await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'POST',
|
||||
path: (this.subscribed ? '/unsubscribe' : '/subscribe'),
|
||||
data: {
|
||||
channelId: this.channelId
|
||||
}
|
||||
})
|
||||
this.subscribed = !this.subscribed
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAuthenticated () {
|
||||
return this.$store.getters['auth/isCurrentlyAuthenticated']
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.checkStatus()
|
||||
},
|
||||
watch: {
|
||||
channelId: 'checkStatus',
|
||||
isAuthenticated: 'checkStatus'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,47 +37,47 @@ import { LibPiped } from '@/tools/libpiped'
|
||||
import marked from 'marked'
|
||||
|
||||
export default {
|
||||
name: 'VideoComment',
|
||||
props: ['comment', 'video', 'subComment'],
|
||||
data: () => ({
|
||||
requestInProgress: false,
|
||||
showChildComments: true,
|
||||
childComments: []
|
||||
}),
|
||||
methods: {
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
async loadReplies () {
|
||||
let nextpage = null
|
||||
this.requestInProgress = true
|
||||
name: 'VideoComment',
|
||||
props: ['comment', 'video', 'subComment'],
|
||||
data: () => ({
|
||||
requestInProgress: false,
|
||||
showChildComments: true,
|
||||
childComments: []
|
||||
}),
|
||||
methods: {
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
async loadReplies () {
|
||||
let nextpage = null
|
||||
this.requestInProgress = true
|
||||
|
||||
while (true) {
|
||||
const replies = await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/nextpage/comments/' + this.video.videoId,
|
||||
params: {
|
||||
nextpage: nextpage || this.comment.repliesPage
|
||||
}
|
||||
})
|
||||
while (true) {
|
||||
const replies = await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/nextpage/comments/' + this.video.videoId,
|
||||
params: {
|
||||
nextpage: nextpage || this.comment.repliesPage
|
||||
}
|
||||
})
|
||||
|
||||
this.childComments = this.childComments.concat(replies.comments)
|
||||
if (replies.nextpage) {
|
||||
nextpage = replies.nextpage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
this.childComments = this.childComments.concat(replies.comments)
|
||||
if (replies.nextpage) {
|
||||
nextpage = replies.nextpage
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
renderedCommentTxt () {
|
||||
return LibPiped.purifyHTML(marked.parseInline(this.comment.commentText, {
|
||||
breaks: true
|
||||
}))
|
||||
}
|
||||
}
|
||||
computed: {
|
||||
renderedCommentTxt () {
|
||||
return LibPiped.purifyHTML(marked.parseInline(this.comment.commentText, {
|
||||
breaks: true
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,62 +26,62 @@ import { LibPiped } from '@/tools/libpiped'
|
||||
import { findLastWatch } from '@/store/watched-videos-db'
|
||||
|
||||
export default {
|
||||
name: 'VideoItem',
|
||||
props: {
|
||||
video: Object,
|
||||
height: Number,
|
||||
width: Number,
|
||||
hideChannel: Boolean,
|
||||
maxHeight: Boolean,
|
||||
srcProgress: Number
|
||||
},
|
||||
data: () => ({
|
||||
alreadyWatched: false,
|
||||
progress: 0
|
||||
}),
|
||||
mounted () {
|
||||
this.findIfVideoWatched()
|
||||
},
|
||||
watch: {
|
||||
'video.videoId': 'findIfVideoWatched',
|
||||
'video.url': 'findIfVideoWatched',
|
||||
srcProgress: 'findIfVideoWatched'
|
||||
},
|
||||
name: 'VideoItem',
|
||||
props: {
|
||||
video: Object,
|
||||
height: Number,
|
||||
width: Number,
|
||||
hideChannel: Boolean,
|
||||
maxHeight: Boolean,
|
||||
srcProgress: Number
|
||||
},
|
||||
data: () => ({
|
||||
alreadyWatched: false,
|
||||
progress: 0
|
||||
}),
|
||||
mounted () {
|
||||
this.findIfVideoWatched()
|
||||
},
|
||||
watch: {
|
||||
'video.videoId': 'findIfVideoWatched',
|
||||
'video.url': 'findIfVideoWatched',
|
||||
srcProgress: 'findIfVideoWatched'
|
||||
},
|
||||
|
||||
methods: {
|
||||
async findIfVideoWatched () {
|
||||
// if it has source progress, it's already seen
|
||||
if (this.srcProgress) {
|
||||
this.alreadyWatched = true
|
||||
this.progress = this.srcProgress
|
||||
return
|
||||
}
|
||||
methods: {
|
||||
async findIfVideoWatched () {
|
||||
// if it has source progress, it's already seen
|
||||
if (this.srcProgress) {
|
||||
this.alreadyWatched = true
|
||||
this.progress = this.srcProgress
|
||||
return
|
||||
}
|
||||
|
||||
let videoId
|
||||
if (this.video.videoId) {
|
||||
videoId = this.video.videoId
|
||||
} else {
|
||||
videoId = LibPiped.determineVideoIdFromPath(this.video.url)
|
||||
}
|
||||
let videoId
|
||||
if (this.video.videoId) {
|
||||
videoId = this.video.videoId
|
||||
} else {
|
||||
videoId = LibPiped.determineVideoIdFromPath(this.video.url)
|
||||
}
|
||||
|
||||
if (videoId) {
|
||||
const lastVideo = await findLastWatch(videoId)
|
||||
if (lastVideo != null) {
|
||||
this.alreadyWatched = true
|
||||
this.progress = lastVideo.progressPcnt
|
||||
} else {
|
||||
this.alreadyWatched = false
|
||||
}
|
||||
}
|
||||
},
|
||||
if (videoId) {
|
||||
const lastVideo = await findLastWatch(videoId)
|
||||
if (lastVideo != null) {
|
||||
this.alreadyWatched = true
|
||||
this.progress = lastVideo.progressPcnt
|
||||
} else {
|
||||
this.alreadyWatched = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
|
||||
timeFormat (...args) {
|
||||
return LibPiped.timeFormat(...args)
|
||||
}
|
||||
}
|
||||
timeFormat (...args) {
|
||||
return LibPiped.timeFormat(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted () {
|
||||
const videoId = this.$route.params.videoId
|
||||
if (videoId) {
|
||||
this.$router.replace({
|
||||
path: '/watch',
|
||||
query: { v: videoId }
|
||||
})
|
||||
}
|
||||
}
|
||||
mounted () {
|
||||
const videoId = this.$route.params.videoId
|
||||
if (videoId) {
|
||||
this.$router.replace({
|
||||
path: '/watch',
|
||||
query: { v: videoId }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+5
-5
@@ -14,9 +14,9 @@ import './registerServiceWorker'
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
vuetify,
|
||||
i18n,
|
||||
store,
|
||||
router,
|
||||
render: h => h(App)
|
||||
vuetify,
|
||||
i18n,
|
||||
store,
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
|
||||
+70
-70
@@ -6,93 +6,93 @@ import ENTranslations from '@/translations/en.json'
|
||||
Vue.use(VueI18n)
|
||||
|
||||
const messages = {
|
||||
en: ENTranslations
|
||||
en: ENTranslations
|
||||
}
|
||||
|
||||
export const i18n = new VueI18n({
|
||||
locale: 'en', // set default locale
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
locale: 'en', // set default locale
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
})
|
||||
|
||||
async function syncStylesPerLanguage (locale) {
|
||||
switch (locale) {
|
||||
// All the latin languages
|
||||
case 'de':
|
||||
case 'en':
|
||||
case 'el':
|
||||
case 'fr':
|
||||
case 'lt':
|
||||
case 'ml':
|
||||
case 'tr':
|
||||
case 'bn_latn':
|
||||
// Don't need to import fonts because Latin fonts are always loaded
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('latin')
|
||||
break
|
||||
// Bengali script
|
||||
case 'bn_beng':
|
||||
await import('@fontsource/hind-siliguri/bengali.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('bengali')
|
||||
break
|
||||
// Other languages
|
||||
// NOTE: if you are a native speaker & want to see a different font, just email me or join the channel
|
||||
case 'zh_Hant':
|
||||
await import('@fontsource/noto-sans-tc/chinese-traditional.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('traditional-chinese')
|
||||
break
|
||||
case 'zh_Hans':
|
||||
await import('@fontsource/noto-sans-sc/chinese-simplified.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('simplified-chinese')
|
||||
break
|
||||
case 'jp':
|
||||
await import('@fontsource/noto-sans-jp/japanese.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('japanese')
|
||||
break
|
||||
case 'kr':
|
||||
await import('@fontsource/noto-sans-kr/korean.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('korean')
|
||||
break
|
||||
case 'cyrl':
|
||||
await import('@fontsource/nunito-sans/cyrillic.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('cyrillic')
|
||||
break
|
||||
}
|
||||
switch (locale) {
|
||||
// All the latin languages
|
||||
case 'de':
|
||||
case 'en':
|
||||
case 'el':
|
||||
case 'fr':
|
||||
case 'lt':
|
||||
case 'ml':
|
||||
case 'tr':
|
||||
case 'bn_latn':
|
||||
// Don't need to import fonts because Latin fonts are always loaded
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('latin')
|
||||
break
|
||||
// Bengali script
|
||||
case 'bn_beng':
|
||||
await import('@fontsource/hind-siliguri/bengali.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('bengali')
|
||||
break
|
||||
// Other languages
|
||||
// NOTE: if you are a native speaker & want to see a different font, just email me or join the channel
|
||||
case 'zh_Hant':
|
||||
await import('@fontsource/noto-sans-tc/chinese-traditional.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('traditional-chinese')
|
||||
break
|
||||
case 'zh_Hans':
|
||||
await import('@fontsource/noto-sans-sc/chinese-simplified.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('simplified-chinese')
|
||||
break
|
||||
case 'jp':
|
||||
await import('@fontsource/noto-sans-jp/japanese.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('japanese')
|
||||
break
|
||||
case 'kr':
|
||||
await import('@fontsource/noto-sans-kr/korean.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('korean')
|
||||
break
|
||||
case 'cyrl':
|
||||
await import('@fontsource/nunito-sans/cyrillic.css')
|
||||
document.body.classList.remove(...document.body.classList)
|
||||
document.body.classList.add('cyrillic')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadLocale (locale) {
|
||||
if (i18n.availableLocales.includes(locale)) {
|
||||
return
|
||||
}
|
||||
// load locale messages with dynamic import
|
||||
const messages = await import(/* webpackChunkName: "locale-[request]" */ `@/translations/${locale}.json`)
|
||||
if (i18n.availableLocales.includes(locale)) {
|
||||
return
|
||||
}
|
||||
// load locale messages with dynamic import
|
||||
const messages = await import(/* webpackChunkName: "locale-[request]" */ `@/translations/${locale}.json`)
|
||||
|
||||
// set locale and locale message
|
||||
i18n.setLocaleMessage(locale, messages.default)
|
||||
// set locale and locale message
|
||||
i18n.setLocaleMessage(locale, messages.default)
|
||||
|
||||
return Vue.nextTick()
|
||||
return Vue.nextTick()
|
||||
}
|
||||
|
||||
export async function changeLocale (lang) {
|
||||
await loadLocale(lang)
|
||||
await syncStylesPerLanguage(lang)
|
||||
i18n.locale = lang
|
||||
window.localStorage.setItem('LOCALE', lang)
|
||||
await loadLocale(lang)
|
||||
await syncStylesPerLanguage(lang)
|
||||
i18n.locale = lang
|
||||
window.localStorage.setItem('LOCALE', lang)
|
||||
}
|
||||
|
||||
function initializeLocalLocale () {
|
||||
let lang = window.localStorage.getItem('LOCALE')
|
||||
if (lang == null) {
|
||||
// Default language
|
||||
lang = 'en'
|
||||
}
|
||||
changeLocale(lang).catch(e => console.error(e))
|
||||
let lang = window.localStorage.getItem('LOCALE')
|
||||
if (lang == null) {
|
||||
// Default language
|
||||
lang = 'en'
|
||||
}
|
||||
changeLocale(lang).catch(e => console.error(e))
|
||||
}
|
||||
|
||||
initializeLocalLocale()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import tinykeys from 'tinykeys'
|
||||
|
||||
export function setupKeybindings (el, bindings, ...extra) {
|
||||
const newBindings = {}
|
||||
for (const [k, f] of Object.entries(bindings)) {
|
||||
newBindings[k] = (ev, ...args) => {
|
||||
const active = ev.target
|
||||
const enteringText = active instanceof HTMLElement &&
|
||||
const newBindings = {}
|
||||
for (const [k, f] of Object.entries(bindings)) {
|
||||
newBindings[k] = (ev, ...args) => {
|
||||
const active = ev.target
|
||||
const enteringText = active instanceof HTMLElement &&
|
||||
(active.isContentEditable ||
|
||||
active.tagName === 'INPUT' ||
|
||||
active.tagName === 'TEXTAREA' ||
|
||||
active.tagName === 'SELECT'
|
||||
)
|
||||
if (!enteringText) return f(ev, ...args)
|
||||
}
|
||||
}
|
||||
if (!enteringText) return f(ev, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
return tinykeys(el, newBindings, ...extra)
|
||||
return tinykeys(el, newBindings, ...extra)
|
||||
}
|
||||
|
||||
+35
-35
@@ -4,41 +4,41 @@ import Vuetify from 'vuetify/lib/framework'
|
||||
Vue.use(Vuetify)
|
||||
|
||||
export default new Vuetify({
|
||||
theme: {
|
||||
// There's apparently a race condition that causes components being rendered shortly before dark mode initialization unable to change to dark mode
|
||||
dark: (() => {
|
||||
try {
|
||||
const preferences = JSON.parse(window.localStorage.getItem('PREFERENCES'))
|
||||
return preferences.darkMode
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#458588',
|
||||
secondary: '#689d6a',
|
||||
accent: '#af3a03',
|
||||
error: '#cc241d',
|
||||
warning: '#d79921',
|
||||
info: '#458588',
|
||||
success: '#98971a',
|
||||
theme: {
|
||||
// There's apparently a race condition that causes components being rendered shortly before dark mode initialization unable to change to dark mode
|
||||
dark: (() => {
|
||||
try {
|
||||
const preferences = JSON.parse(window.localStorage.getItem('PREFERENCES'))
|
||||
return preferences.darkMode
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#458588',
|
||||
secondary: '#689d6a',
|
||||
accent: '#af3a03',
|
||||
error: '#cc241d',
|
||||
warning: '#d79921',
|
||||
info: '#458588',
|
||||
success: '#98971a',
|
||||
|
||||
bgOne: '#fbf1c7',
|
||||
bgTwo: '#ebdbb2'
|
||||
},
|
||||
dark: {
|
||||
primary: '#458588',
|
||||
secondary: '#689d6a',
|
||||
accent: '#af3a03',
|
||||
error: '#cc241d',
|
||||
warning: '#d79921',
|
||||
info: '#458588',
|
||||
success: '#98971a',
|
||||
bgOne: '#fbf1c7',
|
||||
bgTwo: '#ebdbb2'
|
||||
},
|
||||
dark: {
|
||||
primary: '#458588',
|
||||
secondary: '#689d6a',
|
||||
accent: '#af3a03',
|
||||
error: '#cc241d',
|
||||
warning: '#d79921',
|
||||
info: '#458588',
|
||||
success: '#98971a',
|
||||
|
||||
bgOne: '#282828',
|
||||
bgTwo: '#3c3836'
|
||||
}
|
||||
}
|
||||
}
|
||||
bgOne: '#282828',
|
||||
bgTwo: '#3c3836'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
import { register } from 'register-service-worker'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready () {
|
||||
console.log(
|
||||
'App is being served from cache by a service worker.\n' +
|
||||
register(`${process.env.BASE_URL}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)
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+55
-55
@@ -8,64 +8,64 @@ Vue.use(VueMeta)
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Homepage',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/trending',
|
||||
name: 'Trending',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/feed',
|
||||
name: 'Feed',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/preferences',
|
||||
name: 'Preferences',
|
||||
component: () => import('@/routes/Preferences')
|
||||
},
|
||||
{
|
||||
path: '/subscriptions',
|
||||
component: () => import('@/routes/Subscriptions')
|
||||
},
|
||||
{
|
||||
path: '/results',
|
||||
name: 'SearchResults',
|
||||
component: () => import('@/routes/SearchResults')
|
||||
},
|
||||
{
|
||||
path: '/playlist',
|
||||
name: 'Playlist',
|
||||
component: () => import('@/routes/Playlist')
|
||||
},
|
||||
{
|
||||
path: '/:path(v|w|embed|shorts|watch)/:v?',
|
||||
name: 'WatchVideo',
|
||||
component: () => import('@/routes/WatchVideo')
|
||||
},
|
||||
{
|
||||
path: '/:path(channel|user|c)/:channelId/:videos?',
|
||||
name: 'Channel',
|
||||
component: () => import('@/routes/Channel')
|
||||
},
|
||||
{
|
||||
path: '/:videoId([a-zA-Z0-9_-]{11})',
|
||||
component: () => import('@/components/VideoRedirect')
|
||||
},
|
||||
{
|
||||
path: '/watch-history',
|
||||
component: () => import('@/routes/WatchHistory')
|
||||
}
|
||||
{
|
||||
path: '/',
|
||||
name: 'Homepage',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/trending',
|
||||
name: 'Trending',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/feed',
|
||||
name: 'Feed',
|
||||
component: FeedPage
|
||||
},
|
||||
{
|
||||
path: '/preferences',
|
||||
name: 'Preferences',
|
||||
component: () => import('@/routes/Preferences')
|
||||
},
|
||||
{
|
||||
path: '/subscriptions',
|
||||
component: () => import('@/routes/Subscriptions')
|
||||
},
|
||||
{
|
||||
path: '/results',
|
||||
name: 'SearchResults',
|
||||
component: () => import('@/routes/SearchResults')
|
||||
},
|
||||
{
|
||||
path: '/playlist',
|
||||
name: 'Playlist',
|
||||
component: () => import('@/routes/Playlist')
|
||||
},
|
||||
{
|
||||
path: '/:path(v|w|embed|shorts|watch)/:v?',
|
||||
name: 'WatchVideo',
|
||||
component: () => import('@/routes/WatchVideo')
|
||||
},
|
||||
{
|
||||
path: '/:path(channel|user|c)/:channelId/:videos?',
|
||||
name: 'Channel',
|
||||
component: () => import('@/routes/Channel')
|
||||
},
|
||||
{
|
||||
path: '/:videoId([a-zA-Z0-9_-]{11})',
|
||||
component: () => import('@/components/VideoRedirect')
|
||||
},
|
||||
{
|
||||
path: '/watch-history',
|
||||
component: () => import('@/routes/WatchHistory')
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
+93
-93
@@ -42,103 +42,103 @@ import SubscriptionButton from '@/components/SubscriptionButton'
|
||||
import { LibPiped } from '@/tools/libpiped'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
channel: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
const title = this.channel ? this.channel.name : 'Loading'
|
||||
data () {
|
||||
return {
|
||||
channel: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
const title = this.channel ? this.channel.name : 'Loading'
|
||||
|
||||
return {
|
||||
title,
|
||||
meta: this.channel && [
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'profile'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
property: 'og:profile:username',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: this.channel.bannerUrl
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: this.channel.bannerUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
return {
|
||||
title,
|
||||
meta: this.channel && [
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'profile'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
property: 'og:profile:username',
|
||||
content: this.channel.name
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content: this.channel.description
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: this.channel.bannerUrl
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: this.channel.bannerUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.fetchChannel()
|
||||
},
|
||||
watch: {
|
||||
'$route.params.channelId': 'fetchChannel'
|
||||
},
|
||||
methods: {
|
||||
async fetchChannel () {
|
||||
this.channel = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/' + this.$route.params.path + '/' + this.$route.params.channelId
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.fetchChannel()
|
||||
},
|
||||
watch: {
|
||||
'$route.params.channelId': 'fetchChannel'
|
||||
},
|
||||
methods: {
|
||||
async fetchChannel () {
|
||||
this.channel = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/' + this.$route.params.path + '/' + this.$route.params.channelId
|
||||
})
|
||||
},
|
||||
|
||||
onRelatedStreamsEndIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreVideos()
|
||||
}
|
||||
},
|
||||
onRelatedStreamsEndIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreVideos()
|
||||
}
|
||||
},
|
||||
|
||||
fetchMoreVideos () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/channel/' + this.channel.id,
|
||||
params: {
|
||||
nextpage: this.channel.nextpage
|
||||
}
|
||||
}).then(j => {
|
||||
this.channel.relatedStreams = this.channel.relatedStreams.concat(j.relatedStreams)
|
||||
this.channel.nextpage = j.nextpage
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedDescription () {
|
||||
return LibPiped.purifyHTML(marked.parseInline(this.channel.description, {
|
||||
breaks: true
|
||||
}))
|
||||
},
|
||||
fetchMoreVideos () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/channel/' + this.channel.id,
|
||||
params: {
|
||||
nextpage: this.channel.nextpage
|
||||
}
|
||||
}).then(j => {
|
||||
this.channel.relatedStreams = this.channel.relatedStreams.concat(j.relatedStreams)
|
||||
this.channel.nextpage = j.nextpage
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedDescription () {
|
||||
return LibPiped.purifyHTML(marked.parseInline(this.channel.description, {
|
||||
breaks: true
|
||||
}))
|
||||
},
|
||||
|
||||
chunkedByFour () {
|
||||
return _chunk(this.channel.relatedStreams, 4)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SubscriptionButton,
|
||||
ErrorHandler,
|
||||
VideoItem
|
||||
}
|
||||
chunkedByFour () {
|
||||
return _chunk(this.channel.relatedStreams, 4)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SubscriptionButton,
|
||||
ErrorHandler,
|
||||
VideoItem
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+85
-85
@@ -28,97 +28,97 @@ import { chunk as _chunk } from 'lodash-es'
|
||||
import VideoItem from '@/components/VideoItem.vue'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
feedName: 'trending',
|
||||
videos: [],
|
||||
error: null,
|
||||
errorIsJSON: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
feedName: 'trending',
|
||||
videos: [],
|
||||
error: null,
|
||||
errorIsJSON: false
|
||||
}
|
||||
},
|
||||
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.' + this.feedName)
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.' + this.feedName)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.path': 'fetchData'
|
||||
},
|
||||
mounted () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
const region = this.$store.getters['prefs/getPreference']('region', 'US')
|
||||
const selectedHomepage = this.$store.getters['prefs/getPreference']('homepage', 'trending')
|
||||
let path
|
||||
watch: {
|
||||
'$route.path': 'fetchData'
|
||||
},
|
||||
mounted () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
const region = this.$store.getters['prefs/getPreference']('region', 'US')
|
||||
const selectedHomepage = this.$store.getters['prefs/getPreference']('homepage', 'trending')
|
||||
let path
|
||||
|
||||
switch (this.$route.path) {
|
||||
case '/':
|
||||
if (selectedHomepage === 'trending') {
|
||||
path = '/trending'
|
||||
this.feedName = 'trending'
|
||||
} else {
|
||||
path = '/feed'
|
||||
this.feedName = 'feed'
|
||||
}
|
||||
break
|
||||
case '/trending':
|
||||
path = '/trending'
|
||||
this.feedName = 'trending'
|
||||
break
|
||||
case '/feed':
|
||||
path = '/feed'
|
||||
this.feedName = 'feed'
|
||||
break
|
||||
}
|
||||
switch (this.$route.path) {
|
||||
case '/':
|
||||
if (selectedHomepage === 'trending') {
|
||||
path = '/trending'
|
||||
this.feedName = 'trending'
|
||||
} else {
|
||||
path = '/feed'
|
||||
this.feedName = 'feed'
|
||||
}
|
||||
break
|
||||
case '/trending':
|
||||
path = '/trending'
|
||||
this.feedName = 'trending'
|
||||
break
|
||||
case '/feed':
|
||||
path = '/feed'
|
||||
this.feedName = 'feed'
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
this.error = null
|
||||
this.videos = await this.$store.dispatch('auth/makeRequest', {
|
||||
path,
|
||||
params: {
|
||||
region: region
|
||||
},
|
||||
tokenInParams: true
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.isAxiosError) {
|
||||
const rData = e.response.data
|
||||
if (rData.message === 'Could not get Trending name') {
|
||||
this.error = 'errors.trendingFetchError'
|
||||
} else {
|
||||
this.error = rData
|
||||
this.errorIsJSON = true
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rowSize () {
|
||||
return this.$store.getters['prefs/getPreferenceNumber']('feedColumns', 4)
|
||||
},
|
||||
try {
|
||||
this.error = null
|
||||
this.videos = await this.$store.dispatch('auth/makeRequest', {
|
||||
path,
|
||||
params: {
|
||||
region: region
|
||||
},
|
||||
tokenInParams: true
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.isAxiosError) {
|
||||
const rData = e.response.data
|
||||
if (rData.message === 'Could not get Trending name') {
|
||||
this.error = 'errors.trendingFetchError'
|
||||
} else {
|
||||
this.error = rData
|
||||
this.errorIsJSON = true
|
||||
}
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rowSize () {
|
||||
return this.$store.getters['prefs/getPreferenceNumber']('feedColumns', 4)
|
||||
},
|
||||
|
||||
columnClass () {
|
||||
if (!this.$vuetify.breakpoint.mdAndUp) {
|
||||
return
|
||||
}
|
||||
return 'span-col-' + (60 / this.rowSize) + ' mb-4'
|
||||
},
|
||||
columnClass () {
|
||||
if (!this.$vuetify.breakpoint.mdAndUp) {
|
||||
return
|
||||
}
|
||||
return 'span-col-' + (60 / this.rowSize) + ' mb-4'
|
||||
},
|
||||
|
||||
splitIntoRows () {
|
||||
return _chunk(this.videos, this.rowSize)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
VideoItem,
|
||||
JSONViewer: () => import('vue-json-viewer')
|
||||
}
|
||||
splitIntoRows () {
|
||||
return _chunk(this.videos, this.rowSize)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
VideoItem,
|
||||
JSONViewer: () => import('vue-json-viewer')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
+34
-34
@@ -47,41 +47,41 @@ import VideoItem from '@/components/VideoItem.vue'
|
||||
import { chunk as _chunk } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
playlist: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return { title: this.playlist ? this.playlist.name : 'Loading' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
playlist: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return { title: this.playlist ? this.playlist.name : 'Loading' }
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.getPlaylistData()
|
||||
},
|
||||
computed: {
|
||||
getRssUrl () {
|
||||
return this.$store.getters['prefs/apiUrl'] + '/rss/playlists/' + this.$route.query.list
|
||||
},
|
||||
mounted () {
|
||||
this.getPlaylistData()
|
||||
},
|
||||
computed: {
|
||||
getRssUrl () {
|
||||
return this.$store.getters['prefs/apiUrl'] + '/rss/playlists/' + this.$route.query.list
|
||||
},
|
||||
|
||||
chunkedByFour () {
|
||||
return _chunk(this.playlist.relatedStreams, 4)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchPlaylist () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/playlists/' + this.$route.query.list
|
||||
})
|
||||
},
|
||||
async getPlaylistData () {
|
||||
this.fetchPlaylist()
|
||||
.then(data => (this.playlist = data))
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ErrorHandler,
|
||||
VideoItem
|
||||
}
|
||||
chunkedByFour () {
|
||||
return _chunk(this.playlist.relatedStreams, 4)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchPlaylist () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/playlists/' + this.$route.query.list
|
||||
})
|
||||
},
|
||||
async getPlaylistData () {
|
||||
this.fetchPlaylist()
|
||||
.then(data => (this.playlist = data))
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ErrorHandler,
|
||||
VideoItem
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+187
-187
@@ -38,196 +38,196 @@ import EnglishNames from 'i18n-iso-countries/langs/en.json'
|
||||
Countries.registerLocale(EnglishNames)
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
instances: [],
|
||||
data () {
|
||||
return {
|
||||
instances: [],
|
||||
|
||||
options: [
|
||||
{
|
||||
id: 'playerAutoplay',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'listen',
|
||||
type: 'bool',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'disableLBRY',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'proxyLBRY',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'skipToLastPoint',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'homepage',
|
||||
type: 'select',
|
||||
label: 'Default Homepage',
|
||||
default: 'trending',
|
||||
options: [
|
||||
{
|
||||
text: 'Trending',
|
||||
value: 'trending'
|
||||
},
|
||||
{
|
||||
text: 'Feed',
|
||||
value: 'feed'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'feedColumns',
|
||||
type: 'select',
|
||||
label: 'Amount of Columns on Feed Pages',
|
||||
default: 4,
|
||||
options: [
|
||||
{
|
||||
text: '4 columns',
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
text: '5 columns',
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: '6 columns',
|
||||
value: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
type: 'select',
|
||||
default: 0,
|
||||
options: [
|
||||
{
|
||||
text: 'Auto',
|
||||
value: 0
|
||||
},
|
||||
...([144, 240, 360, 480, 720, 1080, 1440, 2160, 4320].map(i => ({
|
||||
text: i.toString() + 'p',
|
||||
value: i
|
||||
})))
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'bufferGoal',
|
||||
type: 'number',
|
||||
default: 10
|
||||
},
|
||||
{
|
||||
id: 'sponsorblock',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'selectedSkip',
|
||||
type: 'select',
|
||||
label: 'Selected Segments to Skip',
|
||||
multi: true,
|
||||
default: ['sponsor', 'interaction', 'selfpromo', 'music_offtopic'],
|
||||
options: [
|
||||
{ text: 'Sponsor Segments ', value: 'sponsors' },
|
||||
{ text: 'Intermission/Intro Animation Segments', value: 'intro' },
|
||||
{ text: 'Endcards/Credits Segments', value: 'outro' },
|
||||
{ text: 'Preview/Recap Segments', value: 'preview' },
|
||||
{ text: 'Interaction Reminder (Subscribe) Segments', value: 'interaction' },
|
||||
{ text: 'Unpaid/Self Promotion Segments', value: 'selfpromo' },
|
||||
{ text: 'Music: Non-Music Segments', value: 'music_offtopic' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
type: 'select',
|
||||
label: 'Country',
|
||||
default: 'US',
|
||||
options: Object.entries(Countries.getNames('en', { select: 'official' })).map(([code, name]) => ({
|
||||
text: name,
|
||||
value: code
|
||||
}))
|
||||
}
|
||||
],
|
||||
tableHeaders: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: 'API URL',
|
||||
value: 'apiurl'
|
||||
},
|
||||
{
|
||||
text: 'Locations',
|
||||
value: 'locations'
|
||||
},
|
||||
{
|
||||
text: 'CDN enabled?',
|
||||
value: 'cdn'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.preferences')
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{
|
||||
id: 'playerAutoplay',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'listen',
|
||||
type: 'bool',
|
||||
default: false
|
||||
},
|
||||
{
|
||||
id: 'disableLBRY',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'proxyLBRY',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'skipToLastPoint',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'homepage',
|
||||
type: 'select',
|
||||
label: 'Default Homepage',
|
||||
default: 'trending',
|
||||
options: [
|
||||
{
|
||||
text: 'Trending',
|
||||
value: 'trending'
|
||||
},
|
||||
{
|
||||
text: 'Feed',
|
||||
value: 'feed'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'feedColumns',
|
||||
type: 'select',
|
||||
label: 'Amount of Columns on Feed Pages',
|
||||
default: 4,
|
||||
options: [
|
||||
{
|
||||
text: '4 columns',
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
text: '5 columns',
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: '6 columns',
|
||||
value: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
type: 'select',
|
||||
default: 0,
|
||||
options: [
|
||||
{
|
||||
text: 'Auto',
|
||||
value: 0
|
||||
},
|
||||
...([144, 240, 360, 480, 720, 1080, 1440, 2160, 4320].map(i => ({
|
||||
text: i.toString() + 'p',
|
||||
value: i
|
||||
})))
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'bufferGoal',
|
||||
type: 'number',
|
||||
default: 10
|
||||
},
|
||||
{
|
||||
id: 'sponsorblock',
|
||||
type: 'bool',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
id: 'selectedSkip',
|
||||
type: 'select',
|
||||
label: 'Selected Segments to Skip',
|
||||
multi: true,
|
||||
default: ['sponsor', 'interaction', 'selfpromo', 'music_offtopic'],
|
||||
options: [
|
||||
{ text: 'Sponsor Segments ', value: 'sponsors' },
|
||||
{ text: 'Intermission/Intro Animation Segments', value: 'intro' },
|
||||
{ text: 'Endcards/Credits Segments', value: 'outro' },
|
||||
{ text: 'Preview/Recap Segments', value: 'preview' },
|
||||
{ text: 'Interaction Reminder (Subscribe) Segments', value: 'interaction' },
|
||||
{ text: 'Unpaid/Self Promotion Segments', value: 'selfpromo' },
|
||||
{ text: 'Music: Non-Music Segments', value: 'music_offtopic' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
type: 'select',
|
||||
label: 'Country',
|
||||
default: 'US',
|
||||
options: Object.entries(Countries.getNames('en', { select: 'official' })).map(([code, name]) => ({
|
||||
text: name,
|
||||
value: code
|
||||
}))
|
||||
}
|
||||
],
|
||||
tableHeaders: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: 'API URL',
|
||||
value: 'apiurl'
|
||||
},
|
||||
{
|
||||
text: 'Locations',
|
||||
value: 'locations'
|
||||
},
|
||||
{
|
||||
text: 'CDN enabled?',
|
||||
value: 'cdn'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.preferences')
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} })
|
||||
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 === 5) {
|
||||
if (skipped < 2) {
|
||||
skipped++
|
||||
return
|
||||
}
|
||||
this.instances.push({
|
||||
name: split[0].trim(),
|
||||
apiurl: split[1].trim(),
|
||||
locations: split[2].trim(),
|
||||
cdn: split[3].trim()
|
||||
})
|
||||
}
|
||||
})
|
||||
this.options.push({
|
||||
id: 'instance',
|
||||
type: 'select',
|
||||
default: this.$store.getters['prefs/apiUrl'],
|
||||
label: 'Instance',
|
||||
options: this.instances.map(i => ({
|
||||
text: i.name,
|
||||
value: i.apiurl
|
||||
}))
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
setValue (k, v) {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: k,
|
||||
value: v
|
||||
})
|
||||
},
|
||||
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 === 5) {
|
||||
if (skipped < 2) {
|
||||
skipped++
|
||||
return
|
||||
}
|
||||
this.instances.push({
|
||||
name: split[0].trim(),
|
||||
apiurl: split[1].trim(),
|
||||
locations: split[2].trim(),
|
||||
cdn: split[3].trim()
|
||||
})
|
||||
}
|
||||
})
|
||||
this.options.push({
|
||||
id: 'instance',
|
||||
type: 'select',
|
||||
default: this.$store.getters['prefs/apiUrl'],
|
||||
label: 'Instance',
|
||||
options: this.instances.map(i => ({
|
||||
text: i.name,
|
||||
value: i.apiurl
|
||||
}))
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
setValue (k, v) {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: k,
|
||||
value: v
|
||||
})
|
||||
},
|
||||
|
||||
sslScore (url) {
|
||||
return 'https://www.ssllabs.com/ssltest/analyze.html?d=' + new URL(url).host + '&latest'
|
||||
}
|
||||
}
|
||||
sslScore (url) {
|
||||
return 'https://www.ssllabs.com/ssltest/analyze.html?d=' + new URL(url).host + '&latest'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+57
-57
@@ -20,66 +20,66 @@
|
||||
import { setupKeybindings } from '@/plugins/keybindings'
|
||||
|
||||
export default {
|
||||
name: 'SearchMenu',
|
||||
data () {
|
||||
return {
|
||||
selected: 0,
|
||||
select: '',
|
||||
searchText: '',
|
||||
requestInProgress: false,
|
||||
searchSuggestions: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchText (val) {
|
||||
if (val && val !== this.select) {
|
||||
this.refreshSuggestions()
|
||||
}
|
||||
},
|
||||
name: 'SearchMenu',
|
||||
data () {
|
||||
return {
|
||||
selected: 0,
|
||||
select: '',
|
||||
searchText: '',
|
||||
requestInProgress: false,
|
||||
searchSuggestions: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchText (val) {
|
||||
if (val && val !== this.select) {
|
||||
this.refreshSuggestions()
|
||||
}
|
||||
},
|
||||
|
||||
async select (val) {
|
||||
if (val === '') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: 'SearchResults',
|
||||
query: { search_query: val }
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('???', e)
|
||||
} finally {
|
||||
this.select = ''
|
||||
this.searchText = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
async select (val) {
|
||||
if (val === '') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.$router.push({
|
||||
name: 'SearchResults',
|
||||
query: { search_query: val }
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('???', e)
|
||||
} finally {
|
||||
this.select = ''
|
||||
this.searchText = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshSuggestions () {
|
||||
this.requestInProgress = true
|
||||
this.searchSuggestions = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/suggestions',
|
||||
params: {
|
||||
query: this.searchText
|
||||
}
|
||||
})
|
||||
this.searchSuggestions.unshift(this.searchText)
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refreshSuggestions () {
|
||||
this.requestInProgress = true
|
||||
this.searchSuggestions = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/suggestions',
|
||||
params: {
|
||||
query: this.searchText
|
||||
}
|
||||
})
|
||||
this.searchSuggestions.unshift(this.searchText)
|
||||
this.requestInProgress = false
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.unsubToKeybindings = setupKeybindings(window, {
|
||||
'/': (e) => {
|
||||
this.$refs.searchMenu.focus()
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
this.unsubToKeybindings = setupKeybindings(window, {
|
||||
'/': (e) => {
|
||||
this.$refs.searchMenu.focus()
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.unsubToKeybindings()
|
||||
}
|
||||
beforeDestroy () {
|
||||
this.unsubToKeybindings()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+102
-102
@@ -31,115 +31,115 @@ import VideoItem from '@/components/VideoItem'
|
||||
import GenericDisplayItem from '@/components/GenericDisplayItem'
|
||||
|
||||
export default {
|
||||
components: { GenericDisplayItem, VideoItem },
|
||||
data () {
|
||||
return {
|
||||
results: null,
|
||||
availableFilters: [
|
||||
'all',
|
||||
'videos',
|
||||
'channels',
|
||||
'playlists',
|
||||
'music_songs',
|
||||
'music_videos',
|
||||
'music_albums',
|
||||
'music_playlists'
|
||||
],
|
||||
selectedFilter: 'all'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$route.query.search_query
|
||||
}
|
||||
},
|
||||
components: { GenericDisplayItem, VideoItem },
|
||||
data () {
|
||||
return {
|
||||
results: null,
|
||||
availableFilters: [
|
||||
'all',
|
||||
'videos',
|
||||
'channels',
|
||||
'playlists',
|
||||
'music_songs',
|
||||
'music_videos',
|
||||
'music_albums',
|
||||
'music_playlists'
|
||||
],
|
||||
selectedFilter: 'all'
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$route.query.search_query
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.updateResults()
|
||||
},
|
||||
computed: {
|
||||
chunkedByFour () {
|
||||
return _chunk(this.results.items, 4)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedFilter () {
|
||||
this.updateResults()
|
||||
},
|
||||
mounted () {
|
||||
this.updateResults()
|
||||
},
|
||||
computed: {
|
||||
chunkedByFour () {
|
||||
return _chunk(this.results.items, 4)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedFilter () {
|
||||
this.updateResults()
|
||||
},
|
||||
|
||||
// For history navigation
|
||||
'$route.query.search_query' () {
|
||||
this.updateResults()
|
||||
}
|
||||
},
|
||||
// For history navigation
|
||||
'$route.query.search_query' () {
|
||||
this.updateResults()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
rationalizeSearchResult (sr) {
|
||||
let type
|
||||
// This seriously can't be the best solution
|
||||
if (sr.url.startsWith('/watch')) {
|
||||
type = 'VIDEO'
|
||||
} else if (sr.url.startsWith('/playlist')) {
|
||||
type = 'PLAYLIST'
|
||||
} else if (sr.url.startsWith('/channel')) {
|
||||
type = 'CHANNEL'
|
||||
} else {
|
||||
console.warn('WARNING: UNKNOWN VIDEO URL TYPE FOUND:', sr.url)
|
||||
type = 'VIDEO'
|
||||
}
|
||||
methods: {
|
||||
rationalizeSearchResult (sr) {
|
||||
let type
|
||||
// This seriously can't be the best solution
|
||||
if (sr.url.startsWith('/watch')) {
|
||||
type = 'VIDEO'
|
||||
} else if (sr.url.startsWith('/playlist')) {
|
||||
type = 'PLAYLIST'
|
||||
} else if (sr.url.startsWith('/channel')) {
|
||||
type = 'CHANNEL'
|
||||
} else {
|
||||
console.warn('WARNING: UNKNOWN VIDEO URL TYPE FOUND:', sr.url)
|
||||
type = 'VIDEO'
|
||||
}
|
||||
|
||||
return {
|
||||
title: sr.name,
|
||||
type,
|
||||
uploaderName: sr.uploader,
|
||||
uploadedDate: sr.uploadDate,
|
||||
...sr
|
||||
}
|
||||
},
|
||||
return {
|
||||
title: sr.name,
|
||||
type,
|
||||
uploaderName: sr.uploader,
|
||||
uploadedDate: sr.uploadDate,
|
||||
...sr
|
||||
}
|
||||
},
|
||||
|
||||
async fetchResults () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: 'search',
|
||||
params: {
|
||||
q: this.$route.query.search_query,
|
||||
filter: this.selectedFilter
|
||||
}
|
||||
})
|
||||
},
|
||||
async fetchResults () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: 'search',
|
||||
params: {
|
||||
q: this.$route.query.search_query,
|
||||
filter: this.selectedFilter
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
|
||||
timeFormat (...args) {
|
||||
return LibPiped.timeFormat(...args)
|
||||
},
|
||||
async updateResults () {
|
||||
this.results = this.fetchResults().then(json => {
|
||||
json.items = json.items.map(this.rationalizeSearchResult)
|
||||
this.results = json
|
||||
})
|
||||
},
|
||||
onSearchResultsEndIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreResults()
|
||||
}
|
||||
},
|
||||
timeFormat (...args) {
|
||||
return LibPiped.timeFormat(...args)
|
||||
},
|
||||
async updateResults () {
|
||||
this.results = this.fetchResults().then(json => {
|
||||
json.items = json.items.map(this.rationalizeSearchResult)
|
||||
this.results = json
|
||||
})
|
||||
},
|
||||
onSearchResultsEndIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreResults()
|
||||
}
|
||||
},
|
||||
|
||||
fetchMoreResults () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/search',
|
||||
params: {
|
||||
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.results.items = this.results.items.concat(json.items.map(this.rationalizeSearchResult))
|
||||
})
|
||||
}
|
||||
}
|
||||
fetchMoreResults () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/search',
|
||||
params: {
|
||||
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.results.items = this.results.items.concat(json.items.map(this.rationalizeSearchResult))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,33 +23,33 @@ import SubscriptionButton from '@/components/SubscriptionButton'
|
||||
import { LibPiped } from '@/tools/libpiped'
|
||||
|
||||
export default {
|
||||
name: 'Subscriptions',
|
||||
components: { SubscriptionButton },
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
data: null
|
||||
}),
|
||||
methods: {
|
||||
async loadData () {
|
||||
if (!this.$store.getters['auth/isCurrentlyAuthenticated']) {
|
||||
await this.$router.replace({
|
||||
path: '/'
|
||||
})
|
||||
return
|
||||
}
|
||||
const resp = await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/subscriptions'
|
||||
})
|
||||
this.loaded = true
|
||||
this.data = _chunk(resp.map(r => {
|
||||
r.id = LibPiped.determineVideoIdFromChannelURL(r.url)
|
||||
return r
|
||||
}), 6)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadData()
|
||||
}
|
||||
name: 'Subscriptions',
|
||||
components: { SubscriptionButton },
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
data: null
|
||||
}),
|
||||
methods: {
|
||||
async loadData () {
|
||||
if (!this.$store.getters['auth/isCurrentlyAuthenticated']) {
|
||||
await this.$router.replace({
|
||||
path: '/'
|
||||
})
|
||||
return
|
||||
}
|
||||
const resp = await this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/subscriptions'
|
||||
})
|
||||
this.loaded = true
|
||||
this.data = _chunk(resp.map(r => {
|
||||
r.id = LibPiped.determineVideoIdFromChannelURL(r.url)
|
||||
return r
|
||||
}), 6)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+41
-41
@@ -30,52 +30,52 @@ import VideoItem from '@/components/VideoItem'
|
||||
import ExpandableDate from '@/components/ExpandableDate'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExpandableDate,
|
||||
VideoItem
|
||||
},
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
data: null,
|
||||
unfinishedVideosSwitch: false
|
||||
}),
|
||||
components: {
|
||||
ExpandableDate,
|
||||
VideoItem
|
||||
},
|
||||
data: () => ({
|
||||
loaded: false,
|
||||
data: null,
|
||||
unfinishedVideosSwitch: false
|
||||
}),
|
||||
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.history')
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.$t('titles.history')
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadData (onlyUnfinished = false) {
|
||||
if (!onlyUnfinished) {
|
||||
this.data = await getWatchedVideos()
|
||||
} else {
|
||||
this.data = await getUnfinishedVideos()
|
||||
}
|
||||
this.loaded = true
|
||||
},
|
||||
methods: {
|
||||
async loadData (onlyUnfinished = false) {
|
||||
if (!onlyUnfinished) {
|
||||
this.data = await getWatchedVideos()
|
||||
} else {
|
||||
this.data = await getUnfinishedVideos()
|
||||
}
|
||||
this.loaded = true
|
||||
},
|
||||
|
||||
async deleteWatchHistory () {
|
||||
await deleteWatchedVideos()
|
||||
await this.loadData()
|
||||
}
|
||||
},
|
||||
async deleteWatchHistory () {
|
||||
await deleteWatchedVideos()
|
||||
await this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
unfinishedVideosSwitch () {
|
||||
this.loadData(this.unfinishedVideosSwitch)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
unfinishedVideosSwitch () {
|
||||
this.loadData(this.unfinishedVideosSwitch)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
chunkedByFour () {
|
||||
return _chunk(this.data, 4)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
chunkedByFour () {
|
||||
return _chunk(this.data, 4)
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadData().catch(e => console.error(e))
|
||||
}
|
||||
mounted () {
|
||||
this.loadData().catch(e => console.error(e))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+205
-205
@@ -105,228 +105,228 @@ import SubscriptionButton from '@/components/SubscriptionButton'
|
||||
import ExpandableDate from '@/components/ExpandableDate'
|
||||
|
||||
export default {
|
||||
name: 'WatchVideo',
|
||||
data () {
|
||||
return {
|
||||
loaded: false,
|
||||
video: {
|
||||
title: 'Loading ...'
|
||||
},
|
||||
sponsors: null,
|
||||
selectedAutoLoop: false,
|
||||
showDesc: true,
|
||||
comments: null,
|
||||
channelId: null,
|
||||
name: 'WatchVideo',
|
||||
data () {
|
||||
return {
|
||||
loaded: false,
|
||||
video: {
|
||||
title: 'Loading ...'
|
||||
},
|
||||
sponsors: null,
|
||||
selectedAutoLoop: false,
|
||||
showDesc: true,
|
||||
comments: null,
|
||||
channelId: null,
|
||||
|
||||
dbID: null,
|
||||
lastWatch: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.video.title,
|
||||
meta: [
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: this.video.title
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'video'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: this.video.title
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: this.video.thumbnailUrl
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: this.video.thumbnailUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
dbID: null,
|
||||
lastWatch: null
|
||||
}
|
||||
},
|
||||
metaInfo () {
|
||||
return {
|
||||
title: this.video.title,
|
||||
meta: [
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: this.video.title
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'video'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: this.video.title
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content: this.video.description
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: this.video.thumbnailUrl
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: this.video.thumbnailUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.initialize()
|
||||
},
|
||||
watch: {
|
||||
'$route.query.v': function (v) {
|
||||
if (v) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
this.initialize()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initialize () {
|
||||
this.getVideoData()
|
||||
this.getSponsors()
|
||||
if (this.$store.getters['prefs/getPreferenceBoolean']('comments', true)) this.getComments()
|
||||
},
|
||||
mounted () {
|
||||
this.initialize()
|
||||
},
|
||||
watch: {
|
||||
'$route.query.v': function (v) {
|
||||
if (v) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
this.initialize()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initialize () {
|
||||
this.getVideoData()
|
||||
this.getSponsors()
|
||||
if (this.$store.getters['prefs/getPreferenceBoolean']('comments', true)) this.getComments()
|
||||
},
|
||||
|
||||
videoEnded () {
|
||||
if (!this.selectedAutoLoop && this.isAutoplayEnabled && this.video.relatedStreams[0]) {
|
||||
this.$router.push({
|
||||
name: 'WatchVideo',
|
||||
query: {
|
||||
v: LibPiped.determineVideoIdFromPath(this.video.relatedStreams[0].url)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
videoEnded () {
|
||||
if (!this.selectedAutoLoop && this.isAutoplayEnabled && this.video.relatedStreams[0]) {
|
||||
this.$router.push({
|
||||
name: 'WatchVideo',
|
||||
query: {
|
||||
v: LibPiped.determineVideoIdFromPath(this.video.relatedStreams[0].url)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onYTClick () {
|
||||
const time = this.$refs.player.getCurrentTime()
|
||||
onYTClick () {
|
||||
const time = this.$refs.player.getCurrentTime()
|
||||
|
||||
const url = new URL('https://youtube.com/watch')
|
||||
url.searchParams.set('v', this.videoId)
|
||||
if (Number.isFinite(time)) {
|
||||
url.searchParams.set('t', time.toFixed(0))
|
||||
}
|
||||
window.location.href = url.href
|
||||
},
|
||||
const url = new URL('https://youtube.com/watch')
|
||||
url.searchParams.set('v', this.videoId)
|
||||
if (Number.isFinite(time)) {
|
||||
url.searchParams.set('t', time.toFixed(0))
|
||||
}
|
||||
window.location.href = url.href
|
||||
},
|
||||
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
numberFormat (...args) {
|
||||
return LibPiped.numberFormat(...args)
|
||||
},
|
||||
|
||||
addCommas (...args) {
|
||||
return LibPiped.addCommas(...args)
|
||||
},
|
||||
addCommas (...args) {
|
||||
return LibPiped.addCommas(...args)
|
||||
},
|
||||
|
||||
fetchVideo () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/streams/' + this.videoId
|
||||
})
|
||||
},
|
||||
fetchVideo () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
method: 'GET',
|
||||
path: '/streams/' + this.videoId
|
||||
})
|
||||
},
|
||||
|
||||
async getSponsors () {
|
||||
if (!this.$store.getters['prefs/getPreference']('sponsorblock', true)) {
|
||||
return
|
||||
}
|
||||
this.sponsors = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/sponsors/' + this.videoId,
|
||||
params: {
|
||||
category: JSON.stringify(this.$store.getters['prefs/getPreference']('selectedSkip', ['sponsor', 'interaction', 'selfpromo', 'music_offtopic']))
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchComments () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/comments/' + this.videoId
|
||||
})
|
||||
},
|
||||
async getSponsors () {
|
||||
if (!this.$store.getters['prefs/getPreference']('sponsorblock', true)) {
|
||||
return
|
||||
}
|
||||
this.sponsors = await this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/sponsors/' + this.videoId,
|
||||
params: {
|
||||
category: JSON.stringify(this.$store.getters['prefs/getPreference']('selectedSkip', ['sponsor', 'interaction', 'selfpromo', 'music_offtopic']))
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchComments () {
|
||||
return this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/comments/' + this.videoId
|
||||
})
|
||||
},
|
||||
|
||||
onCommentsProgressIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreComments()
|
||||
}
|
||||
},
|
||||
onCommentsProgressIntersect (entries) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.fetchMoreComments()
|
||||
}
|
||||
},
|
||||
|
||||
fetchMoreComments () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/comments/' + this.videoId,
|
||||
params: {
|
||||
nextpage: this.comments.nextpage
|
||||
}
|
||||
}).then(json => {
|
||||
this.comments.nextpage = json.nextpage
|
||||
this.comments.comments = this.comments.comments.concat(json.comments)
|
||||
})
|
||||
},
|
||||
fetchMoreComments () {
|
||||
this.$store.dispatch('auth/makeRequest', {
|
||||
path: '/nextpage/comments/' + this.videoId,
|
||||
params: {
|
||||
nextpage: this.comments.nextpage
|
||||
}
|
||||
}).then(json => {
|
||||
this.comments.nextpage = json.nextpage
|
||||
this.comments.comments = this.comments.comments.concat(json.comments)
|
||||
})
|
||||
},
|
||||
|
||||
onAutoplayChg (ev) {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'autoplay',
|
||||
value: ev
|
||||
})
|
||||
},
|
||||
onAutoplayChg (ev) {
|
||||
this.$store.commit('prefs/setPrefs', {
|
||||
id: 'autoplay',
|
||||
value: ev
|
||||
})
|
||||
},
|
||||
|
||||
async getVideoData () {
|
||||
try {
|
||||
this.lastWatch = await findLastWatch(this.videoId)
|
||||
} catch (e) {
|
||||
console.error('Errored while finding last watched', e)
|
||||
}
|
||||
async getVideoData () {
|
||||
try {
|
||||
this.lastWatch = await findLastWatch(this.videoId)
|
||||
} catch (e) {
|
||||
console.error('Errored while finding last watched', e)
|
||||
}
|
||||
|
||||
const video = await this.fetchVideo()
|
||||
video.videoId = this.videoId
|
||||
video.url = this.$route.fullPath
|
||||
this.video = video
|
||||
this.loaded = true
|
||||
const video = await this.fetchVideo()
|
||||
video.videoId = this.videoId
|
||||
video.url = this.$route.fullPath
|
||||
this.video = video
|
||||
this.loaded = true
|
||||
|
||||
if (this.video.error) {
|
||||
return
|
||||
}
|
||||
this.channelId = this.video.uploaderUrl.split('/')[2]
|
||||
if (this.video.error) {
|
||||
return
|
||||
}
|
||||
this.channelId = this.video.uploaderUrl.split('/')[2]
|
||||
|
||||
this.video.description = LibPiped.purifyHTML(
|
||||
this.video.description
|
||||
.replaceAll('http://www.youtube.com', '')
|
||||
.replaceAll('https://www.youtube.com', '')
|
||||
.replaceAll('\n', '<br>')
|
||||
)
|
||||
this.dbID = await addWatchedVideo(video)
|
||||
},
|
||||
getComments () {
|
||||
this.fetchComments().then(data => (this.comments = data))
|
||||
},
|
||||
this.video.description = LibPiped.purifyHTML(
|
||||
this.video.description
|
||||
.replaceAll('http://www.youtube.com', '')
|
||||
.replaceAll('https://www.youtube.com', '')
|
||||
.replaceAll('\n', '<br>')
|
||||
)
|
||||
this.dbID = await addWatchedVideo(video)
|
||||
},
|
||||
getComments () {
|
||||
this.fetchComments().then(data => (this.comments = data))
|
||||
},
|
||||
|
||||
onTimeUpdate: debounce(function onTimeUpdate (e) {
|
||||
if (this.dbID == null || !this.$refs.player) {
|
||||
return
|
||||
}
|
||||
return updateWatchedVideoProgress(this.dbID, this.$refs.player.getCurrentTime(), this.video.duration)
|
||||
}, 500)
|
||||
},
|
||||
computed: {
|
||||
isAutoplayEnabled () {
|
||||
return this.$store.getters['prefs/getPreferenceBoolean']('autoplay', false)
|
||||
},
|
||||
videoId () {
|
||||
return this.$route.query.v || this.$route.params.v
|
||||
},
|
||||
skipToTime () {
|
||||
// 't' in $route.query ? Number($route.query.t) : (lastWatch.progress ? lastWatch.progress : undefined)
|
||||
// 1st Priority - t in query
|
||||
// 2nd Priority - Last Watched Progress, if enabled
|
||||
if ('t' in this.$route.query) {
|
||||
return Number(this.$route.query.t)
|
||||
} else if (this.lastWatch && this.lastWatch.progress != null && this.lastWatch.progress !== 0 && this.$store.getters['prefs/getPreferenceBoolean']('skipToLastPoint', true)) {
|
||||
return this.lastWatch.progress
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
onTimeUpdate: debounce(function onTimeUpdate (e) {
|
||||
if (this.dbID == null || !this.$refs.player) {
|
||||
return
|
||||
}
|
||||
return updateWatchedVideoProgress(this.dbID, this.$refs.player.getCurrentTime(), this.video.duration)
|
||||
}, 500)
|
||||
},
|
||||
computed: {
|
||||
isAutoplayEnabled () {
|
||||
return this.$store.getters['prefs/getPreferenceBoolean']('autoplay', false)
|
||||
},
|
||||
videoId () {
|
||||
return this.$route.query.v || this.$route.params.v
|
||||
},
|
||||
skipToTime () {
|
||||
// 't' in $route.query ? Number($route.query.t) : (lastWatch.progress ? lastWatch.progress : undefined)
|
||||
// 1st Priority - t in query
|
||||
// 2nd Priority - Last Watched Progress, if enabled
|
||||
if ('t' in this.$route.query) {
|
||||
return Number(this.$route.query.t)
|
||||
} else if (this.lastWatch && this.lastWatch.progress != null && this.lastWatch.progress !== 0 && this.$store.getters['prefs/getPreferenceBoolean']('skipToLastPoint', true)) {
|
||||
return this.lastWatch.progress
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
|
||||
lastWatchDurationH () {
|
||||
return LibPiped.timeFormat(this.lastWatch.progress)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ExpandableDate,
|
||||
SubscriptionButton,
|
||||
VideoComment,
|
||||
Player,
|
||||
VideoItem,
|
||||
ErrorHandler
|
||||
}
|
||||
lastWatchDurationH () {
|
||||
return LibPiped.timeFormat(this.lastWatch.progress)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ExpandableDate,
|
||||
SubscriptionButton,
|
||||
VideoComment,
|
||||
Player,
|
||||
VideoItem,
|
||||
ErrorHandler
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+109
-109
@@ -2,136 +2,136 @@ import axios from 'axios'
|
||||
import { isPlainObject as _isPlainObject, set as _set } from 'lodash-es'
|
||||
|
||||
export class AuthenticationError extends Error {
|
||||
constructor (message) {
|
||||
super()
|
||||
this.message = message
|
||||
}
|
||||
constructor (message) {
|
||||
super()
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
const AuthenticationStore = {
|
||||
namespaced: true,
|
||||
namespaced: true,
|
||||
|
||||
state: () => ({
|
||||
authStateByInstance: {
|
||||
// isAuthenticated: Boolean
|
||||
// authToken: String?
|
||||
}
|
||||
}),
|
||||
state: () => ({
|
||||
authStateByInstance: {
|
||||
// isAuthenticated: Boolean
|
||||
// authToken: String?
|
||||
}
|
||||
}),
|
||||
|
||||
mutations: {
|
||||
replaceAuth (state, data) {
|
||||
state.authStateByInstance = data
|
||||
},
|
||||
mutations: {
|
||||
replaceAuth (state, data) {
|
||||
state.authStateByInstance = data
|
||||
},
|
||||
|
||||
setAuthToken (state, { apiURL, token }) {
|
||||
_set(state.authStateByInstance, [apiURL], {
|
||||
isAuthenticated: true,
|
||||
authToken: token
|
||||
})
|
||||
window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
|
||||
},
|
||||
setAuthToken (state, { apiURL, token }) {
|
||||
_set(state.authStateByInstance, [apiURL], {
|
||||
isAuthenticated: true,
|
||||
authToken: token
|
||||
})
|
||||
window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
|
||||
},
|
||||
|
||||
deleteAuthToken (state, { apiURL }) {
|
||||
_set(state.authStateByInstance, [apiURL], {
|
||||
isAuthenticated: false
|
||||
})
|
||||
window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
|
||||
}
|
||||
},
|
||||
deleteAuthToken (state, { apiURL }) {
|
||||
_set(state.authStateByInstance, [apiURL], {
|
||||
isAuthenticated: false
|
||||
})
|
||||
window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
isCurrentlyAuthenticated (state, getters, rootState, rootGetters) {
|
||||
const s = state.authStateByInstance[rootGetters['prefs/apiUrl']]
|
||||
return s ? (s.isAuthenticated === true) : false
|
||||
},
|
||||
getters: {
|
||||
isCurrentlyAuthenticated (state, getters, rootState, rootGetters) {
|
||||
const s = state.authStateByInstance[rootGetters['prefs/apiUrl']]
|
||||
return s ? (s.isAuthenticated === true) : false
|
||||
},
|
||||
|
||||
authToken (state, getters, rootState, rootGetters) {
|
||||
const authState = state.authStateByInstance[rootGetters['prefs/apiUrl']] || {}
|
||||
return authState.authToken
|
||||
}
|
||||
},
|
||||
authToken (state, getters, rootState, rootGetters) {
|
||||
const authState = state.authStateByInstance[rootGetters['prefs/apiUrl']] || {}
|
||||
return authState.authToken
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
initializeAuth ({ commit }) {
|
||||
const data = window.localStorage.getItem('AUTH')
|
||||
if (data != null) {
|
||||
commit('replaceAuth', JSON.parse(data))
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
initializeAuth ({ commit }) {
|
||||
const data = window.localStorage.getItem('AUTH')
|
||||
if (data != null) {
|
||||
commit('replaceAuth', JSON.parse(data))
|
||||
}
|
||||
},
|
||||
|
||||
async loginOrRegister ({ commit, rootGetters }, { path, username, password }) {
|
||||
const apiURL = rootGetters['prefs/apiUrl']
|
||||
const { data: resp } = await axios({
|
||||
method: 'POST',
|
||||
baseURL: apiURL,
|
||||
url: '/' + path,
|
||||
data: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
})
|
||||
async loginOrRegister ({ commit, rootGetters }, { path, username, password }) {
|
||||
const apiURL = rootGetters['prefs/apiUrl']
|
||||
const { data: resp } = await axios({
|
||||
method: 'POST',
|
||||
baseURL: apiURL,
|
||||
url: '/' + path,
|
||||
data: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
})
|
||||
|
||||
if ('error' in resp) {
|
||||
throw new AuthenticationError(resp.error)
|
||||
}
|
||||
if ('error' in resp) {
|
||||
throw new AuthenticationError(resp.error)
|
||||
}
|
||||
|
||||
commit('setAuthToken', {
|
||||
apiURL,
|
||||
token: resp.token
|
||||
})
|
||||
},
|
||||
commit('setAuthToken', {
|
||||
apiURL,
|
||||
token: resp.token
|
||||
})
|
||||
},
|
||||
|
||||
async makeRequest ({
|
||||
commit,
|
||||
state,
|
||||
rootGetters
|
||||
}, {
|
||||
path,
|
||||
method,
|
||||
data,
|
||||
params,
|
||||
tokenInParams = false
|
||||
}) {
|
||||
const APIURL = rootGetters['prefs/apiUrl']
|
||||
const AuthState = state.authStateByInstance[APIURL] || {}
|
||||
async makeRequest ({
|
||||
commit,
|
||||
state,
|
||||
rootGetters
|
||||
}, {
|
||||
path,
|
||||
method,
|
||||
data,
|
||||
params,
|
||||
tokenInParams = false
|
||||
}) {
|
||||
const APIURL = rootGetters['prefs/apiUrl']
|
||||
const AuthState = state.authStateByInstance[APIURL] || {}
|
||||
|
||||
if (AuthState.isAuthenticated && tokenInParams) {
|
||||
if (_isPlainObject(params)) {
|
||||
params.authToken = AuthState.authToken
|
||||
} else {
|
||||
params = {
|
||||
authToken: AuthState.authToken
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AuthState.isAuthenticated && tokenInParams) {
|
||||
if (_isPlainObject(params)) {
|
||||
params.authToken = AuthState.authToken
|
||||
} else {
|
||||
params = {
|
||||
authToken: AuthState.authToken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data: resp } = await axios({
|
||||
baseURL: APIURL,
|
||||
method,
|
||||
url: path,
|
||||
params,
|
||||
data,
|
||||
headers: AuthState.isAuthenticated
|
||||
? {
|
||||
Authorization: AuthState.authToken
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
const { data: resp } = await axios({
|
||||
baseURL: APIURL,
|
||||
method,
|
||||
url: path,
|
||||
params,
|
||||
data,
|
||||
headers: AuthState.isAuthenticated
|
||||
? {
|
||||
Authorization: AuthState.authToken
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeAuthEvents (store) {
|
||||
window.addEventListener('storage', (storageEv) => {
|
||||
if (storageEv.key === 'AUTH') {
|
||||
store.commit('auth/replaceAuth', JSON.parse(storageEv.newValue))
|
||||
}
|
||||
})
|
||||
window.addEventListener('storage', (storageEv) => {
|
||||
if (storageEv.key === 'AUTH') {
|
||||
store.commit('auth/replaceAuth', JSON.parse(storageEv.newValue))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
AuthenticationStore,
|
||||
initializeAuthEvents
|
||||
AuthenticationStore,
|
||||
initializeAuthEvents
|
||||
}
|
||||
|
||||
+4
-4
@@ -7,10 +7,10 @@ import { AuthenticationStore, initializeAuthEvents } from '@/store/authenticatio
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
prefs: PrefsStore,
|
||||
auth: AuthenticationStore
|
||||
}
|
||||
modules: {
|
||||
prefs: PrefsStore,
|
||||
auth: AuthenticationStore
|
||||
}
|
||||
})
|
||||
|
||||
initializePrefEvents(store)
|
||||
|
||||
+76
-76
@@ -1,93 +1,93 @@
|
||||
import { get as _get, isString, set as _set } from 'lodash-es'
|
||||
|
||||
const PrefsStore = {
|
||||
namespaced: true,
|
||||
state: () => ({
|
||||
// Declaring boolean values here because v-simple-checkbox has a bug (???) which renders it unable to have internal state
|
||||
prefs: {
|
||||
darkMode: false,
|
||||
playerAutoplay: true,
|
||||
autoplay: false,
|
||||
listen: false,
|
||||
disableLBRY: true,
|
||||
proxyLBRY: true,
|
||||
sponsorblock: true,
|
||||
skipToLastPoint: true
|
||||
}
|
||||
}),
|
||||
mutations: {
|
||||
setPrefs (state, {
|
||||
id,
|
||||
value
|
||||
}) {
|
||||
_set(state.prefs, id, value)
|
||||
window.localStorage.setItem('PREFERENCES', JSON.stringify(state.prefs))
|
||||
},
|
||||
namespaced: true,
|
||||
state: () => ({
|
||||
// Declaring boolean values here because v-simple-checkbox has a bug (???) which renders it unable to have internal state
|
||||
prefs: {
|
||||
darkMode: false,
|
||||
playerAutoplay: true,
|
||||
autoplay: false,
|
||||
listen: false,
|
||||
disableLBRY: true,
|
||||
proxyLBRY: true,
|
||||
sponsorblock: true,
|
||||
skipToLastPoint: true
|
||||
}
|
||||
}),
|
||||
mutations: {
|
||||
setPrefs (state, {
|
||||
id,
|
||||
value
|
||||
}) {
|
||||
_set(state.prefs, id, value)
|
||||
window.localStorage.setItem('PREFERENCES', JSON.stringify(state.prefs))
|
||||
},
|
||||
|
||||
replacePrefs (state, nextPrefs) {
|
||||
state.prefs = nextPrefs
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
loadState ({ commit }) {
|
||||
try {
|
||||
const jv = window.localStorage.getItem('PREFERENCES')
|
||||
if (isString(jv) && jv.length !== 0) {
|
||||
const p = JSON.parse(jv)
|
||||
commit('replacePrefs', p)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
replacePrefs (state, nextPrefs) {
|
||||
state.prefs = nextPrefs
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
loadState ({ commit }) {
|
||||
try {
|
||||
const jv = window.localStorage.getItem('PREFERENCES')
|
||||
if (isString(jv) && jv.length !== 0) {
|
||||
const p = JSON.parse(jv)
|
||||
commit('replacePrefs', p)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error:', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
getPreference: state => (id, default_) => {
|
||||
return _get(state.prefs, id, default_)
|
||||
},
|
||||
getters: {
|
||||
getPreference: state => (id, default_) => {
|
||||
return _get(state.prefs, id, default_)
|
||||
},
|
||||
|
||||
getPreferenceBoolean: (_, getters) => (...args) => {
|
||||
const v = getters.getPreference(...args)
|
||||
getPreferenceBoolean: (_, getters) => (...args) => {
|
||||
const v = getters.getPreference(...args)
|
||||
|
||||
switch (v) {
|
||||
case 'true':
|
||||
case true:
|
||||
case '1':
|
||||
case 'on':
|
||||
case 'yes':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
switch (v) {
|
||||
case 'true':
|
||||
case true:
|
||||
case '1':
|
||||
case 'on':
|
||||
case 'yes':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
getPreferenceNumber: (_, getters) => (id, default_) => {
|
||||
const v = getters.getPreference(id, default_)
|
||||
getPreferenceNumber: (_, getters) => (id, default_) => {
|
||||
const v = getters.getPreference(id, default_)
|
||||
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) {
|
||||
return default_
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
},
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n)) {
|
||||
return default_
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
},
|
||||
|
||||
apiUrl (state, getters) {
|
||||
return getters.getPreference('instance', process.env.VUE_APP_PIPED_URL || 'https://pipedapi.kavin.rocks')
|
||||
}
|
||||
}
|
||||
apiUrl (state, getters) {
|
||||
return getters.getPreference('instance', process.env.VUE_APP_PIPED_URL || 'https://pipedapi.kavin.rocks')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializePrefEvents (store) {
|
||||
window.addEventListener('storage', (storageEv) => {
|
||||
if (storageEv.key === 'PREFERENCES') {
|
||||
store.commit('prefs/replacePrefs', JSON.parse(storageEv.newValue))
|
||||
}
|
||||
})
|
||||
window.addEventListener('storage', (storageEv) => {
|
||||
if (storageEv.key === 'PREFERENCES') {
|
||||
store.commit('prefs/replacePrefs', JSON.parse(storageEv.newValue))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
PrefsStore,
|
||||
initializePrefEvents
|
||||
PrefsStore,
|
||||
initializePrefEvents
|
||||
}
|
||||
|
||||
@@ -3,39 +3,39 @@ import Dexie from 'dexie'
|
||||
export const PMDB = new Dexie('PipedMaterialDB')
|
||||
|
||||
PMDB.version(3).stores({
|
||||
watchedVideos: '++id,videoId,progressPcnt,timestamp'
|
||||
watchedVideos: '++id,videoId,progressPcnt,timestamp'
|
||||
})
|
||||
|
||||
export async function addWatchedVideo (videoObj) {
|
||||
return PMDB.watchedVideos.add({
|
||||
videoId: videoObj.videoId,
|
||||
video: videoObj,
|
||||
progress: 0,
|
||||
progressPcnt: 0,
|
||||
timestamp: new Date()
|
||||
})
|
||||
return PMDB.watchedVideos.add({
|
||||
videoId: videoObj.videoId,
|
||||
video: videoObj,
|
||||
progress: 0,
|
||||
progressPcnt: 0,
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateWatchedVideoProgress (videoID, prog, dur) {
|
||||
return PMDB.watchedVideos.update(videoID, {
|
||||
progress: prog,
|
||||
progressPcnt: Math.min((prog / dur) * 100, 100)
|
||||
})
|
||||
return PMDB.watchedVideos.update(videoID, {
|
||||
progress: prog,
|
||||
progressPcnt: Math.min((prog / dur) * 100, 100)
|
||||
})
|
||||
}
|
||||
|
||||
export function findLastWatch (videoId) {
|
||||
return PMDB.watchedVideos.where('videoId').equals(videoId).last()
|
||||
return PMDB.watchedVideos.where('videoId').equals(videoId).last()
|
||||
}
|
||||
|
||||
export function getWatchedVideos () {
|
||||
return PMDB.watchedVideos.orderBy('timestamp').reverse().toArray()
|
||||
return PMDB.watchedVideos.orderBy('timestamp').reverse().toArray()
|
||||
}
|
||||
|
||||
export function getUnfinishedVideos () {
|
||||
// Shaka never fires the last event, thus the last percent turns out to be around 99.95 or 99.98
|
||||
return PMDB.watchedVideos.where('progressPcnt').below(99.9).reverse().sortBy('timestamp')
|
||||
// Shaka never fires the last event, thus the last percent turns out to be around 99.95 or 99.98
|
||||
return PMDB.watchedVideos.where('progressPcnt').below(99.9).reverse().sortBy('timestamp')
|
||||
}
|
||||
|
||||
export function deleteWatchedVideos () {
|
||||
return PMDB.watchedVideos.clear()
|
||||
return PMDB.watchedVideos.clear()
|
||||
}
|
||||
|
||||
+178
-178
@@ -3,185 +3,185 @@
|
||||
import xml from '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) {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
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) {
|
||||
return {
|
||||
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}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
generate_representation_video (Format) {
|
||||
return {
|
||||
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}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
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) {
|
||||
return {
|
||||
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}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
generate_representation_video (Format) {
|
||||
return {
|
||||
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}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DashUtils
|
||||
DashUtils
|
||||
}
|
||||
|
||||
+67
-55
@@ -7,74 +7,86 @@ TimeAgo.addDefaultLocale(en)
|
||||
const timeAgo = new TimeAgo('en-US')
|
||||
|
||||
class LibPiped {
|
||||
intlDTF = new Intl.DateTimeFormat([], {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'full'
|
||||
})
|
||||
intlDTF = new Intl.DateTimeFormat([], {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'full'
|
||||
})
|
||||
|
||||
pad (num, size) {
|
||||
return ('000' + num).slice(size * -1)
|
||||
}
|
||||
pad (num, size) {
|
||||
return ('000' + num).slice(size * -1)
|
||||
}
|
||||
|
||||
timeFormat (duration) {
|
||||
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)
|
||||
timeFormat (duration) {
|
||||
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 = ''
|
||||
let str = ''
|
||||
|
||||
if (hours > 0) str += hours + ':'
|
||||
if (hours > 0) str += hours + ':'
|
||||
|
||||
str += this.pad(minutes, 2) + ':' + this.pad(seconds, 2)
|
||||
str += this.pad(minutes, 2) + ':' + this.pad(seconds, 2)
|
||||
|
||||
return str
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
formatFullDate (date) {
|
||||
return this.intlDTF.format(date)
|
||||
}
|
||||
formatFullDate (date) {
|
||||
return this.intlDTF.format(date)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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')
|
||||
}
|
||||
addCommas (num) {
|
||||
num = parseInt(num)
|
||||
return num.toLocaleString('en-US')
|
||||
}
|
||||
|
||||
purifyHTML (original) {
|
||||
return DOMPurify.sanitize(original)
|
||||
}
|
||||
purifyHTML (original) {
|
||||
return DOMPurify.sanitize(original)
|
||||
}
|
||||
|
||||
timeAgo (time) {
|
||||
return timeAgo.format(time)
|
||||
}
|
||||
timeAgo (time) {
|
||||
return timeAgo.format(time)
|
||||
}
|
||||
|
||||
determineVideoIdFromPath (path) {
|
||||
const loc = new URL(path, 'http://localhost')
|
||||
return loc.searchParams.get('v')
|
||||
}
|
||||
determineVideoIdFromPath (path) {
|
||||
const loc = new URL(path, 'http://localhost')
|
||||
return loc.searchParams.get('v')
|
||||
}
|
||||
|
||||
determineVideoIdFromChannelURL (path) {
|
||||
const pathParts = path.split('/')
|
||||
return pathParts[2]
|
||||
}
|
||||
determineVideoIdFromChannelURL (path) {
|
||||
const pathParts = path.split('/')
|
||||
return pathParts[2]
|
||||
}
|
||||
}
|
||||
|
||||
const lp = new LibPiped()
|
||||
|
||||
+22
-22
@@ -1,26 +1,26 @@
|
||||
module.exports = {
|
||||
transpileDependencies: [
|
||||
'vuetify'
|
||||
],
|
||||
pwa: {
|
||||
name: 'Piped Material',
|
||||
themeColor: '#458588',
|
||||
msTileColor: '',
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
appleMobileWebAppStatusBarStyle: 'black',
|
||||
transpileDependencies: [
|
||||
'vuetify'
|
||||
],
|
||||
pwa: {
|
||||
name: 'Piped Material',
|
||||
themeColor: '#458588',
|
||||
msTileColor: '',
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
appleMobileWebAppStatusBarStyle: 'black',
|
||||
|
||||
workboxOptions: {
|
||||
navigateFallback: 'index.html',
|
||||
skipWaiting: true,
|
||||
importWorkboxFrom: 'local',
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /\.(?:png|svg|ico)$/,
|
||||
handler: 'CacheFirst'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
workboxOptions: {
|
||||
navigateFallback: 'index.html',
|
||||
skipWaiting: true,
|
||||
importWorkboxFrom: 'local',
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /\.(?:png|svg|ico)$/,
|
||||
handler: 'CacheFirst'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
lintOnSave: false
|
||||
lintOnSave: false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user