This commit is contained in:
root
2021-12-15 13:22:12 +05:30
parent 8ca3e9cbe2
commit affa225fae
35 changed files with 2129 additions and 2074 deletions
-5
View File
@@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+41 -41
View File
@@ -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>
+7 -7
View File
@@ -8,12 +8,12 @@
<script>
export default {
props: {
error: String,
message: String
},
data: () => ({
showStacktrace: false
})
props: {
error: String,
message: String
},
data: () => ({
showStacktrace: false
})
}
</script>
+10 -10
View File
@@ -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>
+11 -11
View File
@@ -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
View File
@@ -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>
+43 -43
View File
@@ -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>
+39 -39
View File
@@ -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>
+52 -52
View File
@@ -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>
+9 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+9 -9
View File
@@ -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
View File
@@ -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'
}
}
}
})
+26 -26
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+28 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+17 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}