Add full playlist functionality (Shuffle, loop, autoplay)
This commit is contained in:
parent
1faa075a7b
commit
8980dc74d2
|
@ -34,9 +34,9 @@ At this time, here is the list of things to do/need to do:
|
||||||
- [x] Playlist View
|
- [x] Playlist View
|
||||||
- [x] Video Watch Page (Recommendations, Comments)
|
- [x] Video Watch Page (Recommendations, Comments)
|
||||||
- [ ] Video player logic (Switching formats / quality, live video, fallback logic)
|
- [ ] Video player logic (Switching formats / quality, live video, fallback logic)
|
||||||
- [ ] Playlist logic (Autoplay next video, shuffle list)
|
- [x] Playlist logic (Autoplay next video, shuffle list)
|
||||||
- [ ] Database Setup and Logic (Updating and creating data)
|
- [x] Database Setup and Logic (Updating and creating data)
|
||||||
- [ ] Settings Page
|
- [x] Settings Page
|
||||||
- [ ] Subscriptions Page and Logic
|
- [ ] Subscriptions Page and Logic
|
||||||
- [ ] Playlists Page (Will allow for creating user playlists. Will replace the "Favorites" Page)
|
- [ ] Playlists Page (Will allow for creating user playlists. Will replace the "Favorites" Page)
|
||||||
- [ ] History Page and Logic
|
- [ ] History Page and Logic
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
|
@ -11,18 +11,18 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
"@fortawesome/vue-fontawesome": "^0.1.9",
|
||||||
"@silvermine/videojs-quality-selector": "^1.2.3",
|
"@silvermine/videojs-quality-selector": "^1.2.4",
|
||||||
"autolinker": "^3.14.1",
|
"autolinker": "^3.14.1",
|
||||||
"bulma-pro": "^0.1.8",
|
"bulma-pro": "^0.2.0",
|
||||||
"dateformat": "^3.0.3",
|
"dateformat": "^3.0.3",
|
||||||
"electron-context-menu": "^1.0.0",
|
"electron-context-menu": "^2.0.1",
|
||||||
"jquery": "^3.5.0",
|
"jquery": "^3.5.1",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"mediaelement": "^4.2.16",
|
"mediaelement": "^4.2.16",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"opml-to-json": "0.0.3",
|
"opml-to-json": "0.0.3",
|
||||||
"video.js": "^7.7.5",
|
"video.js": "^7.7.6",
|
||||||
"videojs-abloop": "^1.1.2",
|
"videojs-abloop": "^1.1.2",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-http-source-selector": "^1.1.6",
|
"videojs-http-source-selector": "^1.1.6",
|
||||||
|
@ -31,38 +31,38 @@
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-electron": "^1.0.6",
|
"vue-electron": "^1.0.6",
|
||||||
"vue-router": "^3.1.6",
|
"vue-router": "^3.1.6",
|
||||||
"vuex": "^3.2.0",
|
"vuex": "^3.4.0",
|
||||||
"xml2json": "^0.12.0",
|
"xml2json": "^0.12.0",
|
||||||
"youtube-chat": "^1.0.2",
|
"youtube-chat": "^1.1.0",
|
||||||
"youtube-comments-fetch": "^1.0.1",
|
"youtube-comments-fetch": "^1.0.1",
|
||||||
"youtube-comments-task": "^1.3.14",
|
"youtube-comments-task": "^1.3.14",
|
||||||
"youtube-suggest": "^1.1.0",
|
"youtube-suggest": "^1.1.0",
|
||||||
"yt-xml2vtt": "^1.0.1",
|
"yt-xml2vtt": "^1.0.1",
|
||||||
"ytdl-core": "^2.1.0",
|
"ytdl-core": "^2.1.2",
|
||||||
"ytpl": "^0.1.20",
|
"ytpl": "^0.1.21",
|
||||||
"ytsr": "^0.1.12"
|
"ytsr": "^0.1.13"
|
||||||
},
|
},
|
||||||
"description": "A private YouTube client",
|
"description": "A private YouTube client",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.6",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
|
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.6",
|
||||||
"@babel/preset-typescript": "^7.9.0",
|
"@babel/preset-typescript": "^7.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.29.0",
|
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||||
"@typescript-eslint/parser": "^2.29.0",
|
"@typescript-eslint/parser": "^2.33.0",
|
||||||
"acorn": "^7.1.1",
|
"acorn": "^7.2.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^6.0.1",
|
||||||
"css-loader": "^3.5.2",
|
"css-loader": "^3.5.3",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"electron": "^8.2.3",
|
"electron": "^8.3.0",
|
||||||
"electron-builder": "^22.5.1",
|
"electron-builder": "^22.6.0",
|
||||||
"electron-debug": "^3.0.1",
|
"electron-debug": "^3.0.1",
|
||||||
"electron-rebuild": "^1.10.1",
|
"electron-rebuild": "^1.11.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^7.0.0",
|
||||||
"eslint-config-prettier": "^6.10.1",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"eslint-config-standard": "^14.1.1",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
@ -72,26 +72,26 @@
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
"fast-glob": "^3.2.2",
|
"fast-glob": "^3.2.2",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"html-webpack-plugin": "^4.2.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"jest": "^25.4.0",
|
"jest": "^26.0.1",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.0.4",
|
"prettier": "^2.0.5",
|
||||||
"sass": "^1.26.3",
|
"sass": "^1.26.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"style-loader": "^1.1.4",
|
"style-loader": "^1.2.1",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.9.2",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.0",
|
||||||
"vue-devtools": "^5.1.3",
|
"vue-devtools": "^5.1.3",
|
||||||
"vue-eslint-parser": "^7.0.0",
|
"vue-eslint-parser": "^7.1.0",
|
||||||
"vue-loader": "^15.9.1",
|
"vue-loader": "^15.9.2",
|
||||||
"vue-style-loader": "^4.1.2",
|
"vue-style-loader": "^4.1.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.10.3"
|
"webpack-dev-server": "^3.11.0"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"main": "./dist/main.js",
|
"main": "./dist/main.js",
|
||||||
|
|
|
@ -7,6 +7,10 @@ export default Vue.extend({
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
playlistId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
forceListType: {
|
forceListType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
@ -16,7 +20,6 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
thumbnail: '',
|
|
||||||
channelName: '',
|
channelName: '',
|
||||||
channelId: '',
|
channelId: '',
|
||||||
viewCount: 0,
|
viewCount: 0,
|
||||||
|
@ -37,6 +40,19 @@ export default Vue.extend({
|
||||||
|
|
||||||
thumbnailPreference: function () {
|
thumbnailPreference: function () {
|
||||||
return this.$store.getters.getThumbnailPreference
|
return this.$store.getters.getThumbnailPreference
|
||||||
|
},
|
||||||
|
|
||||||
|
thumbnail: function () {
|
||||||
|
switch (this.thumbnailPreference) {
|
||||||
|
case 'start':
|
||||||
|
return `https://i.ytimg.com/vi/${this.id}/mq1.jpg`
|
||||||
|
case 'middle':
|
||||||
|
return `https://i.ytimg.com/vi/${this.id}/mq2.jpg`
|
||||||
|
case 'end':
|
||||||
|
return `https://i.ytimg.com/vi/${this.id}/mq3.jpg`
|
||||||
|
default:
|
||||||
|
return `https://i.ytimg.com/vi/${this.id}/mqdefault.jpg`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -54,11 +70,27 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
play: function () {
|
play: function () {
|
||||||
|
const playlistInfo = {
|
||||||
|
playlistId: this.playlistId
|
||||||
|
}
|
||||||
|
console.log('playlist info')
|
||||||
|
console.log(playlistInfo)
|
||||||
|
|
||||||
|
if (this.playlistId !== null) {
|
||||||
|
console.log('Sending playlist info')
|
||||||
|
this.$router.push(
|
||||||
|
{
|
||||||
|
path: `/watch/${this.id}`,
|
||||||
|
query: playlistInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log('no playlist found')
|
||||||
this.$router.push({ path: `/watch/${this.id}` })
|
this.$router.push({ path: `/watch/${this.id}` })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
goToChannel: function () {
|
goToChannel: function () {
|
||||||
console.log(this.data)
|
|
||||||
this.$router.push({ path: `/channel/${this.channelId}` })
|
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -101,20 +133,7 @@ export default Vue.extend({
|
||||||
this.id = this.data.videoId
|
this.id = this.data.videoId
|
||||||
this.title = this.data.title
|
this.title = this.data.title
|
||||||
// this.thumbnail = this.data.videoThumbnails[4].url
|
// this.thumbnail = this.data.videoThumbnails[4].url
|
||||||
switch (this.thumbnailPreference) {
|
|
||||||
case 'start':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq1.jpg`
|
|
||||||
break
|
|
||||||
case 'middle':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq2.jpg`
|
|
||||||
break
|
|
||||||
case 'end':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq3.jpg`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mqdefault.jpg`
|
|
||||||
break
|
|
||||||
}
|
|
||||||
this.channelName = this.data.author
|
this.channelName = this.data.author
|
||||||
this.channelId = this.data.authorId
|
this.channelId = this.data.authorId
|
||||||
this.duration = this.calculateVideoDuration(this.data.lengthSeconds)
|
this.duration = this.calculateVideoDuration(this.data.lengthSeconds)
|
||||||
|
@ -142,22 +161,6 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.title = this.data.title
|
this.title = this.data.title
|
||||||
// this.thumbnail = this.data.thumbnail
|
|
||||||
|
|
||||||
switch (this.thumbnailPreference) {
|
|
||||||
case 'start':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq1.jpg`
|
|
||||||
break
|
|
||||||
case 'middle':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq2.jpg`
|
|
||||||
break
|
|
||||||
case 'end':
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mq3.jpg`
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this.thumbnail = `https://i.ytimg.com/vi/${this.id}/mqdefault.jpg`
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof (this.data.author) === 'string') {
|
if (typeof (this.data.author) === 'string') {
|
||||||
this.channelName = this.data.author
|
this.channelName = this.data.author
|
||||||
|
@ -188,7 +191,7 @@ export default Vue.extend({
|
||||||
this.hideViews = true
|
this.hideViews = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof (this.data.uploaded_at) !== 'undefined' && this.data.uploaded_at.includes('watching')) {
|
if (typeof (this.data.uploaded_at) !== 'undefined' && this.data.uploaded_at !== null && this.data.uploaded_at.includes('watching')) {
|
||||||
const uploadSplit = this.data.uploaded_at.split(' ')
|
const uploadSplit = this.data.uploaded_at.split(' ')
|
||||||
this.viewCount = parseInt(uploadSplit[0])
|
this.viewCount = parseInt(uploadSplit[0])
|
||||||
this.isLive = true
|
this.isLive = true
|
||||||
|
|
|
@ -37,7 +37,10 @@
|
||||||
:style="{width: progressPercentage + '%'}"
|
:style="{width: progressPercentage + '%'}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="videoTitle">
|
<p
|
||||||
|
class="videoTitle"
|
||||||
|
@click="play(id)"
|
||||||
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
|
@ -49,30 +52,35 @@
|
||||||
<span
|
<span
|
||||||
v-if="!isLive && !hideViews"
|
v-if="!isLive && !hideViews"
|
||||||
class="viewCount"
|
class="viewCount"
|
||||||
|
@click="play(id)"
|
||||||
>
|
>
|
||||||
{{ viewCount }} views
|
{{ viewCount }} views
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="uploadedTime !== '' && !isLive"
|
v-if="uploadedTime !== '' && !isLive"
|
||||||
class="uploadedTime"
|
class="uploadedTime"
|
||||||
|
@click="play(id)"
|
||||||
>
|
>
|
||||||
- {{ uploadedTime }}
|
- {{ uploadedTime }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="isLive"
|
v-if="isLive"
|
||||||
class="viewCount"
|
class="viewCount"
|
||||||
|
@click="play(id)"
|
||||||
>
|
>
|
||||||
{{ viewCount }} watching
|
{{ viewCount }} watching
|
||||||
</span>
|
</span>
|
||||||
<p
|
<p
|
||||||
v-if="listType !== 'grid'"
|
v-if="listType !== 'grid'"
|
||||||
class="description"
|
class="description"
|
||||||
|
@click="play(id)"
|
||||||
>
|
>
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
v-if="isLive"
|
v-if="isLive"
|
||||||
class="liveText"
|
class="liveText"
|
||||||
|
@click="play(id)"
|
||||||
>
|
>
|
||||||
LIVE NOW
|
LIVE NOW
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -100,12 +100,80 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getDefaultPlayback
|
return this.$store.getters.getDefaultPlayback
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defaultQuality: function () {
|
||||||
|
return this.$store.getters.getDefaultQuality
|
||||||
|
},
|
||||||
|
|
||||||
defaultVideoFormat: function () {
|
defaultVideoFormat: function () {
|
||||||
return this.$store.getters.getDefaultVideoFormat
|
return this.$store.getters.getDefaultVideoFormat
|
||||||
},
|
},
|
||||||
|
|
||||||
autoplayVideos: function () {
|
autoplayVideos: function () {
|
||||||
return this.$store.getters.getAutoplayVideos
|
return this.$store.getters.getAutoplayVideos
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedDefaultQuality: function () {
|
||||||
|
let selectedQuality = null
|
||||||
|
|
||||||
|
const maxAvailableQuality = parseInt(this.sourceList[this.sourceList.length - 1].qualityLabel.replace(/p|k/, ''))
|
||||||
|
|
||||||
|
switch (maxAvailableQuality) {
|
||||||
|
case 4:
|
||||||
|
if (this.defaultQuality >= 2160) {
|
||||||
|
return '4k'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
if (this.defaultQuality >= 4320) {
|
||||||
|
return '8k'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 144:
|
||||||
|
if (this.defaultQuality >= 144) {
|
||||||
|
return '144p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 240:
|
||||||
|
if (this.defaultQuality >= 240) {
|
||||||
|
return '240p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 360:
|
||||||
|
if (this.defaultQuality >= 360) {
|
||||||
|
return '360p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 480:
|
||||||
|
if (this.defaultQuality >= 480) {
|
||||||
|
return '480p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 720:
|
||||||
|
if (this.defaultQuality >= 720) {
|
||||||
|
return '720p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 1080:
|
||||||
|
if (this.defaultQuality >= 1080) {
|
||||||
|
return '1080p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 1440:
|
||||||
|
if (this.defaultQuality >= 1440) {
|
||||||
|
return '1440p'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return maxAvailableQuality + 'p'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSourceList.forEach((source) => {
|
||||||
|
if (this.determineDefaultQuality(source.qualityLabel)) {
|
||||||
|
selectedQuality = source.qualityLabel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return selectedQuality
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -172,6 +240,10 @@ export default Vue.extend({
|
||||||
|
|
||||||
const v = this
|
const v = this
|
||||||
|
|
||||||
|
this.player.on('ended', function () {
|
||||||
|
v.$emit('ended')
|
||||||
|
})
|
||||||
|
|
||||||
this.player.on('error', function (error, message) {
|
this.player.on('error', function (error, message) {
|
||||||
v.$emit('error', error.target.player.error_)
|
v.$emit('error', error.target.player.error_)
|
||||||
})
|
})
|
||||||
|
@ -191,6 +263,27 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
determineDefaultQuality: function (label) {
|
||||||
|
if (label.includes('p')) {
|
||||||
|
const selectedQuality = parseInt(label.replace('p', ''))
|
||||||
|
return this.defaultQuality === selectedQuality
|
||||||
|
} else if (label.includes('k')) {
|
||||||
|
const hdQuality = parseInt(label.replace('k', ''))
|
||||||
|
|
||||||
|
switch (hdQuality) {
|
||||||
|
case 4:
|
||||||
|
return this.defaultQuality === 2160
|
||||||
|
case 8:
|
||||||
|
return this.defaultQuality === 4320
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Invalid label')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
enableDashFormat: function () {
|
enableDashFormat: function () {
|
||||||
if (this.dashSrc === null) {
|
if (this.dashSrc === null) {
|
||||||
console.log('No dash format available.')
|
console.log('No dash format available.')
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
:src="source.url"
|
:src="source.url"
|
||||||
:type="source.type || source.mimeType"
|
:type="source.type || source.mimeType"
|
||||||
:label="source.qualityLabel"
|
:label="source.qualityLabel"
|
||||||
|
:selected="source.qualityLabel === selectedDefaultQuality"
|
||||||
/>
|
/>
|
||||||
<track
|
<track
|
||||||
v-for="(caption, index) in captionList"
|
v-for="(caption, index) in captionList"
|
||||||
|
|
|
@ -42,15 +42,15 @@ export default Vue.extend({
|
||||||
],
|
],
|
||||||
qualityValues: [
|
qualityValues: [
|
||||||
'auto',
|
'auto',
|
||||||
'144',
|
144,
|
||||||
'240',
|
240,
|
||||||
'360',
|
360,
|
||||||
'480',
|
480,
|
||||||
'720',
|
720,
|
||||||
'1080',
|
1080,
|
||||||
'1440',
|
1440,
|
||||||
'4k',
|
2160,
|
||||||
'8k'
|
4320
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,8 +16,8 @@ export default Vue.extend({
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
id: '',
|
id: '',
|
||||||
|
randomVideoId: '',
|
||||||
title: '',
|
title: '',
|
||||||
thumbnail: '',
|
|
||||||
channelThumbnail: '',
|
channelThumbnail: '',
|
||||||
channelName: '',
|
channelName: '',
|
||||||
channelId: '',
|
channelId: '',
|
||||||
|
@ -25,6 +25,7 @@ export default Vue.extend({
|
||||||
viewCount: 0,
|
viewCount: 0,
|
||||||
lastUpdated: '',
|
lastUpdated: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
infoSource: '',
|
||||||
shareHeaders: [
|
shareHeaders: [
|
||||||
'Copy YouTube Link',
|
'Copy YouTube Link',
|
||||||
'Open in YouTube',
|
'Open in YouTube',
|
||||||
|
@ -42,17 +43,35 @@ export default Vue.extend({
|
||||||
computed: {
|
computed: {
|
||||||
listType: function () {
|
listType: function () {
|
||||||
return this.$store.getters.getListType
|
return this.$store.getters.getListType
|
||||||
|
},
|
||||||
|
|
||||||
|
thumbnailPreference: function () {
|
||||||
|
return this.$store.getters.getThumbnailPreference
|
||||||
|
},
|
||||||
|
|
||||||
|
thumbnail: function () {
|
||||||
|
switch (this.thumbnailPreference) {
|
||||||
|
case 'start':
|
||||||
|
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq1.jpg`
|
||||||
|
case 'middle':
|
||||||
|
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq2.jpg`
|
||||||
|
case 'end':
|
||||||
|
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq3.jpg`
|
||||||
|
default:
|
||||||
|
return `https://i.ytimg.com/vi/${this.randomVideoId}/mqdefault.jpg`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
console.log(this.data)
|
console.log(this.data)
|
||||||
this.id = this.data.id
|
this.id = this.data.id
|
||||||
|
this.randomVideoId = this.data.randomVideoId
|
||||||
this.title = this.data.title
|
this.title = this.data.title
|
||||||
this.thumbnail = this.data.thumbnail
|
|
||||||
this.channelName = this.data.channelName
|
this.channelName = this.data.channelName
|
||||||
this.channelThumbnail = this.data.channelThumbnail
|
this.channelThumbnail = this.data.channelThumbnail
|
||||||
this.uploadedTime = this.data.uploaded_at
|
this.uploadedTime = this.data.uploaded_at
|
||||||
this.description = this.data.description
|
this.description = this.data.description
|
||||||
|
this.infoSource = this.data.infoSource
|
||||||
|
|
||||||
// Causes errors if not put inside of a check
|
// Causes errors if not put inside of a check
|
||||||
if (typeof (this.data.viewCount) !== 'undefined') {
|
if (typeof (this.data.viewCount) !== 'undefined') {
|
||||||
|
|
|
@ -11,7 +11,11 @@
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ videoCount }} videos - {{ viewCount }} views - Last updated on {{ lastUpdated }}
|
{{ videoCount }} videos - {{ viewCount }} views -
|
||||||
|
<span v-if="infoSource !== 'local'">
|
||||||
|
Last updated on
|
||||||
|
</span>
|
||||||
|
{{ lastUpdated }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{ description }}
|
{{ description }}
|
||||||
|
|
|
@ -44,11 +44,11 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
likeCount: {
|
likeCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
default: 0
|
||||||
},
|
},
|
||||||
dislikeCount: {
|
dislikeCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
@ -88,6 +88,10 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getInvidiousInstance
|
return this.$store.getters.getInvidiousInstance
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usingElectron: function () {
|
||||||
|
return this.$store.getters.getUsingElectron
|
||||||
|
},
|
||||||
|
|
||||||
invidiousUrl: function () {
|
invidiousUrl: function () {
|
||||||
return `${this.invidiousInstance}/watch?v=${this.id}`
|
return `${this.invidiousInstance}/watch?v=${this.id}`
|
||||||
},
|
},
|
||||||
|
@ -118,7 +122,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goToChannel: function () {
|
goToChannel: function () {
|
||||||
console.log('TODO: Handle goToChannel')
|
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSubscription: function () {
|
handleSubscription: function () {
|
||||||
|
@ -126,9 +130,6 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFormatChange: function (format) {
|
handleFormatChange: function (format) {
|
||||||
console.log('Handling share')
|
|
||||||
console.log(this)
|
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'dash':
|
case 'dash':
|
||||||
this.$parent.enableDashFormat()
|
this.$parent.enableDashFormat()
|
||||||
|
@ -147,19 +148,28 @@ export default Vue.extend({
|
||||||
navigator.clipboard.writeText(this.youtubeUrl)
|
navigator.clipboard.writeText(this.youtubeUrl)
|
||||||
break
|
break
|
||||||
case 'openYoutube':
|
case 'openYoutube':
|
||||||
// shell.openExternal(this.youtubeUrl)
|
if (this.usingElectron) {
|
||||||
|
const shell = require('electron').shell
|
||||||
|
shell.openExternal(this.youtubeUrl)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'copyYoutubeEmbed':
|
case 'copyYoutubeEmbed':
|
||||||
navigator.clipboard.writeText(this.youtubeEmbedUrl)
|
navigator.clipboard.writeText(this.youtubeEmbedUrl)
|
||||||
break
|
break
|
||||||
case 'openYoutubeEmbed':
|
case 'openYoutubeEmbed':
|
||||||
// shell.openExternal(this.youtubeEmbedUrl)
|
if (this.usingElectron) {
|
||||||
|
const shell = require('electron').shell
|
||||||
|
shell.openExternal(this.youtubeEmbedUrl)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'copyInvidious':
|
case 'copyInvidious':
|
||||||
navigator.clipboard.writeText(this.invidiousUrl)
|
navigator.clipboard.writeText(this.invidiousUrl)
|
||||||
break
|
break
|
||||||
case 'openInvidious':
|
case 'openInvidious':
|
||||||
// shell.openExternal(this.invidiousUrl)
|
if (this.usingElectron) {
|
||||||
|
const shell = require('electron').shell
|
||||||
|
shell.openExternal(this.invidiousUrl)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channelName {
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistIndex {
|
||||||
|
position: relative;
|
||||||
|
bottom: 15px;
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistIcon {
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: -25px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
transition: background 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistIcon:hover {
|
||||||
|
background-color: var(--side-nav-hover-color);
|
||||||
|
transition: background 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistIconActive {
|
||||||
|
color: var(--accent-color)
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistItems {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 395px;
|
||||||
|
margin-top: -15px;
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-right: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistItem {
|
||||||
|
height: 75px;
|
||||||
|
width: 100%;
|
||||||
|
transition: background 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistItem:hover {
|
||||||
|
background-color: var(--side-nav-hover-color);
|
||||||
|
transition: background 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoIndex {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
top: 15px;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoIndexIcon {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
font-size: 14px;
|
||||||
|
top: 32px;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--tertiary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoInfo {
|
||||||
|
margin-left: 30px;
|
||||||
|
position: relative;
|
||||||
|
bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list {
|
||||||
|
height: 60px;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoThumbnail {
|
||||||
|
width: 100px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoThumbnail img {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoTitle {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 105px;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .channelName {
|
||||||
|
margin-left: 105px;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .viewCount {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import FtLoader from '../ft-loader/ft-loader.vue'
|
||||||
|
import FtCard from '../ft-card/ft-card.vue'
|
||||||
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'WatchVideoPlaylist',
|
||||||
|
components: {
|
||||||
|
'ft-loader': FtLoader,
|
||||||
|
'ft-card': FtCard,
|
||||||
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-list-video': FtListVideo
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
playlistId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
videoId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
shuffleEnabled: false,
|
||||||
|
loopEnabled: false,
|
||||||
|
channelName: '',
|
||||||
|
channelId: '',
|
||||||
|
channelThumbnail: '',
|
||||||
|
playlistTitle: '',
|
||||||
|
playlistItems: [],
|
||||||
|
playlistWatchedVideoList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
usingElectron: function () {
|
||||||
|
return this.$store.getters.getUsingElectron
|
||||||
|
},
|
||||||
|
|
||||||
|
backendPreference: function () {
|
||||||
|
return this.$store.getters.getBackendPreference
|
||||||
|
},
|
||||||
|
|
||||||
|
backendFallback: function () {
|
||||||
|
return this.$store.getters.getBackendFallback
|
||||||
|
},
|
||||||
|
|
||||||
|
currentVideoIndex: function () {
|
||||||
|
const index = this.playlistItems.findIndex((item) => {
|
||||||
|
return item.videoId === this.videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
return index + 1
|
||||||
|
},
|
||||||
|
|
||||||
|
playlistVideoCount: function () {
|
||||||
|
return this.playlistItems.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
videoId () {
|
||||||
|
this.playlistWatchedVideoList.push(this.videoId)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
if (this.usingElectron) {
|
||||||
|
this.getPlaylistInformationInvidious()
|
||||||
|
} else {
|
||||||
|
switch (this.backendPreference) {
|
||||||
|
case 'local':
|
||||||
|
this.getPlaylistInformationLocal()
|
||||||
|
break
|
||||||
|
case 'invidious':
|
||||||
|
this.getPlaylistInformationInvidious()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goToPlaylist: function () {
|
||||||
|
this.$router.push({ path: `/playlist/${this.playlistId}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
goToChannel: function () {
|
||||||
|
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleLoop: function () {
|
||||||
|
if (this.loopEnabled) {
|
||||||
|
this.loopEnabled = false
|
||||||
|
console.log('Disabling loop')
|
||||||
|
} else {
|
||||||
|
this.loopEnabled = true
|
||||||
|
console.log('Enabling loop')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleShuffle: function () {
|
||||||
|
if (this.shuffleEnabled) {
|
||||||
|
this.shuffleEnabled = false
|
||||||
|
console.log('Disabling shuffle')
|
||||||
|
} else {
|
||||||
|
this.shuffleEnabled = true
|
||||||
|
console.log('Enabling shuffle')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
playNextVideo: function () {
|
||||||
|
const playlistInfo = {
|
||||||
|
playlistId: this.playlistId
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoIndex = this.playlistItems.findIndex((item) => {
|
||||||
|
return item.videoId === this.videoId
|
||||||
|
})
|
||||||
|
|
||||||
|
const videosRemain = this.playlistWatchedVideoList.length < this.playlistItems.length
|
||||||
|
|
||||||
|
if (this.shuffleEnabled && videosRemain) {
|
||||||
|
let runLoop = true
|
||||||
|
while (runLoop) {
|
||||||
|
const randomInt = Math.floor(Math.random() * this.playlistItems.length)
|
||||||
|
const randomVideoId = this.playlistItems[randomInt].videoId
|
||||||
|
|
||||||
|
const watchedIndex = this.playlistWatchedVideoList.findIndex((watchedVideo) => {
|
||||||
|
return watchedVideo === randomVideoId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (watchedIndex === -1) {
|
||||||
|
runLoop = false
|
||||||
|
this.$router.push(
|
||||||
|
{
|
||||||
|
path: `/watch/${randomVideoId}`,
|
||||||
|
query: playlistInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.shuffleEnabled && !videosRemain) {
|
||||||
|
if (this.loopEnabled) {
|
||||||
|
let runLoop = true
|
||||||
|
while (runLoop) {
|
||||||
|
const randomInt = Math.floor(Math.random() * this.playlistItems.length)
|
||||||
|
const randomVideoId = this.playlistItems[randomInt].videoId
|
||||||
|
|
||||||
|
if (this.videoId !== randomVideoId) {
|
||||||
|
this.playlistItems = []
|
||||||
|
runLoop = false
|
||||||
|
this.$router.push(
|
||||||
|
{
|
||||||
|
path: `/watch/${randomVideoId}`,
|
||||||
|
query: playlistInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.loopEnabled && videoIndex === this.playlistItems.length - 1) {
|
||||||
|
this.$router.push(
|
||||||
|
{
|
||||||
|
path: `/watch/${this.playlistItems[0].videoId}`,
|
||||||
|
query: playlistInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else if (videoIndex < this.playlistItems.length - 1 && !videosRemain) {
|
||||||
|
this.$router.push(
|
||||||
|
{
|
||||||
|
path: `/watch/${this.playlistItems[videoIndex + 1].videoId}`,
|
||||||
|
query: playlistInfo
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaylistInformationLocal: function () {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.$store.dispatch('ytGetPlaylistInfo', this.playlistId).then((result) => {
|
||||||
|
console.log('done')
|
||||||
|
console.log(result)
|
||||||
|
|
||||||
|
this.playlistTitle = result.title
|
||||||
|
this.playlistItems = result.items
|
||||||
|
this.videoCount = result.total_items
|
||||||
|
this.channelName = result.author.name
|
||||||
|
this.channelThumbnail = result.author.avatar
|
||||||
|
this.channelId = result.author.id
|
||||||
|
|
||||||
|
this.playlistItems = result.items
|
||||||
|
|
||||||
|
this.playlistWatchedVideoList.push(this.videoId)
|
||||||
|
this.isLoading = false
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
if (this.backendPreference === 'local' && this.backendFallback) {
|
||||||
|
console.log('Falling back to Invidious API')
|
||||||
|
this.getPlaylistInformationInvidious()
|
||||||
|
} else {
|
||||||
|
this.isLoading = false
|
||||||
|
// TODO: Show toast with error message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaylistInformationInvidious: function () {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
resource: 'playlists',
|
||||||
|
id: this.playlistId,
|
||||||
|
params: {
|
||||||
|
page: this.playlistPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('invidiousGetPlaylistInfo', payload).then((result) => {
|
||||||
|
console.log('done')
|
||||||
|
console.log(result)
|
||||||
|
|
||||||
|
this.playlistTitle = result.title
|
||||||
|
this.videoCount = result.videoCount
|
||||||
|
this.channelName = result.author
|
||||||
|
this.channelThumbnail = result.authorThumbnails[2].url
|
||||||
|
this.channelId = result.authorId
|
||||||
|
this.playlistItems = this.playlistItems.concat(result.videos)
|
||||||
|
|
||||||
|
if (this.playlistItems.length < result.videoCount) {
|
||||||
|
console.log('getting next page')
|
||||||
|
this.playlistPage++
|
||||||
|
this.getPlaylistInformationInvidious()
|
||||||
|
} else {
|
||||||
|
this.playlistWatchedVideoList.push(this.videoId)
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
if (this.backendPreference === 'invidious' && this.backendFallback) {
|
||||||
|
console.log('Error getting data with Invidious, falling back to local backend')
|
||||||
|
this.getPlaylistInformationLocal()
|
||||||
|
} else {
|
||||||
|
this.isLoading = false
|
||||||
|
// TODO: Show toast with error message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<ft-card class="relative">
|
||||||
|
<ft-loader
|
||||||
|
v-if="isLoading"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="pointer"
|
||||||
|
@click="goToPlaylist"
|
||||||
|
>
|
||||||
|
{{ playlistTitle }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
class="channelName"
|
||||||
|
@click="goToChannel"
|
||||||
|
>
|
||||||
|
{{ channelName }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="playlistIndex"
|
||||||
|
>
|
||||||
|
- {{ currentVideoIndex }} / {{ playlistVideoCount }}
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="playlistIcon"
|
||||||
|
:class="{ playlistIconActive: loopEnabled }"
|
||||||
|
icon="retweet"
|
||||||
|
@click="toggleLoop"
|
||||||
|
/>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="playlistIcon"
|
||||||
|
:class="{ playlistIconActive: shuffleEnabled }"
|
||||||
|
icon="random"
|
||||||
|
@click="toggleShuffle"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<ft-flex-box
|
||||||
|
v-if="!isLoading"
|
||||||
|
class="playlistItems"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in playlistItems"
|
||||||
|
:key="index"
|
||||||
|
class="playlistItem"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="item.videoId === videoId"
|
||||||
|
class="videoIndexIcon"
|
||||||
|
icon="play"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="videoIndex"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}
|
||||||
|
</p>
|
||||||
|
<ft-list-video
|
||||||
|
:data="item"
|
||||||
|
:playlist-id="playlistId"
|
||||||
|
force-list-type="list"
|
||||||
|
class="videoInfo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ft-flex-box>
|
||||||
|
</div>
|
||||||
|
</ft-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./watch-video-playlist.js" />
|
||||||
|
<style scoped src="./watch-video-playlist.css" />
|
|
@ -5,3 +5,29 @@
|
||||||
.videoRecommendation {
|
.videoRecommendation {
|
||||||
margin-bottom: -15px;
|
margin-bottom: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/deep/ .list {
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoThumbnail {
|
||||||
|
width: 180px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoThumbnail img {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .videoTitle {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 185px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .channelName {
|
||||||
|
margin-left: 185px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .list .viewCount {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
|
@ -14,11 +14,6 @@ export default Vue.extend({
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
test: 'hello'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
listType: function () {
|
listType: function () {
|
||||||
return this.$store.getters.getListType
|
return this.$store.getters.getListType
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import Datastore from 'nedb'
|
||||||
|
|
||||||
|
let dbLocation
|
||||||
|
|
||||||
|
if (window && window.process && window.process.type === 'renderer') {
|
||||||
|
// Electron is being used
|
||||||
|
let dbLocation = localStorage.getItem('dbLocation')
|
||||||
|
|
||||||
|
if (dbLocation === null) {
|
||||||
|
const electron = require('electron')
|
||||||
|
dbLocation = electron.remote.app.getPath('userData')
|
||||||
|
}
|
||||||
|
|
||||||
|
dbLocation += '/playlists.db'
|
||||||
|
} else {
|
||||||
|
dbLocation = 'playlists.db'
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDb = new Datastore({
|
||||||
|
filename: dbLocation,
|
||||||
|
autoload: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
activePlaylistId: '',
|
||||||
|
activePlaylistVideoList: [],
|
||||||
|
watchedVideosWithinPlaylist: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
addSubscription (state, payload) {
|
||||||
|
state.subscriptions.push(payload)
|
||||||
|
},
|
||||||
|
setSubscriptions (state, payload) {
|
||||||
|
state.subscriptions = payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
addSubscriptions ({ commit }, payload) {
|
||||||
|
subDb.insert(payload, (err, payload) => {
|
||||||
|
if (!err) {
|
||||||
|
commit('addSubscription', payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getSubscriptions ({ commit }, payload) {
|
||||||
|
subDb.find({}, (err, payload) => {
|
||||||
|
if (!err) {
|
||||||
|
commit('setSubscriptions', payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeSubscription ({ commit }, channelId) {
|
||||||
|
subDb.remove({ channelId: channelId }, {}, () => {
|
||||||
|
commit('setSubscriptions', this.state.subscriptions.filter(sub => sub.channelId !== channelId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getters = {}
|
||||||
|
export default {
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations
|
||||||
|
}
|
|
@ -118,7 +118,7 @@ const actions = {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log(playlistId)
|
console.log(playlistId)
|
||||||
console.log('Getting playlist info please wait...')
|
console.log('Getting playlist info please wait...')
|
||||||
ytpl(playlistId, (err, result) => {
|
ytpl(playlistId, { limit: 0 }, (err, result) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
.vjs-modal-dialog .vjs-modal-dialog-content, .video-js .vjs-modal-dialog, .vjs-button > .vjs-icon-placeholder:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
|
.vjs-modal-dialog .vjs-modal-dialog-content, .video-js .vjs-modal-dialog, .vjs-button > .vjs-icon-placeholder:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 0px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default Vue.extend({
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
playlistId: '',
|
playlistId: null,
|
||||||
nextPageRef: '',
|
nextPageRef: '',
|
||||||
lastSearchQuery: '',
|
lastSearchQuery: '',
|
||||||
playlistPage: 1,
|
playlistPage: 1,
|
||||||
|
@ -82,13 +82,14 @@ export default Vue.extend({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
thumbnail: result.items[randomVideoIndex].thumbnail,
|
randomVideoId: result.items[randomVideoIndex].id,
|
||||||
viewCount: result.views,
|
viewCount: result.views,
|
||||||
videoCount: result.total_items,
|
videoCount: result.total_items,
|
||||||
lastUpdated: result.last_updated,
|
lastUpdated: result.last_updated,
|
||||||
channelName: result.author.name,
|
channelName: result.author.name,
|
||||||
channelThumbnail: result.author.avatar,
|
channelThumbnail: result.author.avatar,
|
||||||
channelId: result.author.id
|
channelId: result.author.id,
|
||||||
|
infoSource: 'local'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playlistItems = result.items
|
this.playlistItems = result.items
|
||||||
|
@ -127,12 +128,13 @@ export default Vue.extend({
|
||||||
id: result.playlistId,
|
id: result.playlistId,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
description: result.description,
|
description: result.description,
|
||||||
thumbnail: result.videos[randomVideoIndex].videoThumbnails[0].url,
|
randomVideoId: result.videos[randomVideoIndex].videoId,
|
||||||
viewCount: result.viewCount,
|
viewCount: result.viewCount,
|
||||||
videoCount: result.videoCount,
|
videoCount: result.videoCount,
|
||||||
channelName: result.author,
|
channelName: result.author,
|
||||||
channelThumbnail: result.authorThumbnails[2].url,
|
channelThumbnail: result.authorThumbnails[2].url,
|
||||||
channelId: result.authorId
|
channelId: result.authorId,
|
||||||
|
infoSource: 'invidious'
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateString = new Date(result.updated * 1000)
|
const dateString = new Date(result.updated * 1000)
|
||||||
|
|
|
@ -9,18 +9,21 @@
|
||||||
:data="infoData"
|
:data="infoData"
|
||||||
class="playlistInfo"
|
class="playlistInfo"
|
||||||
/>
|
/>
|
||||||
<ft-flex-box
|
<ft-card
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
class="playlistItems"
|
class="playlistItems"
|
||||||
>
|
>
|
||||||
|
<ft-flex-box>
|
||||||
<ft-list-video
|
<ft-list-video
|
||||||
v-for="(item, index) in playlistItems"
|
v-for="(item, index) in playlistItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
:data="item"
|
:data="item"
|
||||||
|
:playlist-id="playlistId"
|
||||||
force-list-type="list"
|
force-list-type="list"
|
||||||
class="playlistItem"
|
class="playlistItem"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
</ft-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -27,14 +27,40 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchVideoRecommendations {
|
.watchVideoSideBar {
|
||||||
width: 27%;
|
width: 27%;
|
||||||
max-width: 425px;
|
max-width: 425px;
|
||||||
float: right;
|
float: right;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 70px;
|
}
|
||||||
|
|
||||||
|
.watchVideoPlaylist {
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
top: 70px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theatrePlaylist {
|
||||||
|
float: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 85%;
|
||||||
|
height: 500px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: none;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchVideoRecommendations {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchVideoRecommendationsNoCard {
|
||||||
|
top: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchVideoRecommendationsLowerCard {
|
||||||
|
top: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theatreRecommendations {
|
.theatreRecommendations {
|
||||||
|
@ -78,6 +104,14 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.watchVideoPlaylist {
|
||||||
|
float: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 85%;
|
||||||
|
max-width: none;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
.watchVideoRecommendations {
|
.watchVideoRecommendations {
|
||||||
float: none;
|
float: none;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import FtVideoPlayer from '../../components/ft-video-player/ft-video-player.vue'
|
||||||
import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue'
|
import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue'
|
||||||
import WatchVideoDescription from '../../components/watch-video-description/watch-video-description.vue'
|
import WatchVideoDescription from '../../components/watch-video-description/watch-video-description.vue'
|
||||||
import WatchVideoComments from '../../components/watch-video-comments/watch-video-comments.vue'
|
import WatchVideoComments from '../../components/watch-video-comments/watch-video-comments.vue'
|
||||||
|
import WatchVideoPlaylist from '../../components/watch-video-playlist/watch-video-playlist.vue'
|
||||||
import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue'
|
import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
@ -20,6 +21,7 @@ export default Vue.extend({
|
||||||
'watch-video-info': WatchVideoInfo,
|
'watch-video-info': WatchVideoInfo,
|
||||||
'watch-video-description': WatchVideoDescription,
|
'watch-video-description': WatchVideoDescription,
|
||||||
'watch-video-comments': WatchVideoComments,
|
'watch-video-comments': WatchVideoComments,
|
||||||
|
'watch-video-playlist': WatchVideoPlaylist,
|
||||||
'watch-video-recommendations': WatchVideoRecommendations,
|
'watch-video-recommendations': WatchVideoRecommendations,
|
||||||
},
|
},
|
||||||
data: function() {
|
data: function() {
|
||||||
|
@ -31,6 +33,7 @@ export default Vue.extend({
|
||||||
showLegacyPlayer: false,
|
showLegacyPlayer: false,
|
||||||
showYouTubeNoCookieEmbed: false,
|
showYouTubeNoCookieEmbed: false,
|
||||||
hidePlayer: false,
|
hidePlayer: false,
|
||||||
|
isLive: false,
|
||||||
activeFormat: 'legacy',
|
activeFormat: 'legacy',
|
||||||
videoId: '',
|
videoId: '',
|
||||||
videoTitle: '',
|
videoTitle: '',
|
||||||
|
@ -49,6 +52,8 @@ export default Vue.extend({
|
||||||
videoSourceList: [],
|
videoSourceList: [],
|
||||||
captionSourceList: [],
|
captionSourceList: [],
|
||||||
recommendedVideos: [],
|
recommendedVideos: [],
|
||||||
|
watchingPlaylist: false,
|
||||||
|
playlistId: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -111,6 +116,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
this.firstLoad = true
|
this.firstLoad = true
|
||||||
|
|
||||||
|
this.checkIfPlaylist()
|
||||||
|
|
||||||
switch (this.backendPreference) {
|
switch (this.backendPreference) {
|
||||||
case 'local':
|
case 'local':
|
||||||
this.getVideoInformationLocal(this.videoId)
|
this.getVideoInformationLocal(this.videoId)
|
||||||
|
@ -132,6 +139,8 @@ export default Vue.extend({
|
||||||
this.activeFormat = this.defaultVideoFormat
|
this.activeFormat = this.defaultVideoFormat
|
||||||
this.useTheatreMode = this.defaultTheatreMode
|
this.useTheatreMode = this.defaultTheatreMode
|
||||||
|
|
||||||
|
this.checkIfPlaylist()
|
||||||
|
|
||||||
if (!this.usingElectron) {
|
if (!this.usingElectron) {
|
||||||
this.getVideoInformationInvidious()
|
this.getVideoInformationInvidious()
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,6 +167,7 @@ export default Vue.extend({
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('ytGetVideoInformation', this.videoId)
|
.dispatch('ytGetVideoInformation', this.videoId)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
console.log(result)
|
||||||
this.videoTitle = result.title
|
this.videoTitle = result.title
|
||||||
this.videoViewCount = parseInt(
|
this.videoViewCount = parseInt(
|
||||||
result.player_response.videoDetails.viewCount,
|
result.player_response.videoDetails.viewCount,
|
||||||
|
@ -170,9 +180,24 @@ export default Vue.extend({
|
||||||
this.videoDescription =
|
this.videoDescription =
|
||||||
result.player_response.videoDetails.shortDescription
|
result.player_response.videoDetails.shortDescription
|
||||||
this.recommendedVideos = result.related_videos
|
this.recommendedVideos = result.related_videos
|
||||||
this.videoSourceList = result.player_response.streamingData.formats
|
|
||||||
this.videoLikeCount = result.likes
|
this.videoLikeCount = result.likes
|
||||||
this.videoDislikeCount = result.dislikes
|
this.videoDislikeCount = result.dislikes
|
||||||
|
this.isLive = result.player_response.videoDetails.isLive
|
||||||
|
|
||||||
|
if (this.isLive) {
|
||||||
|
this.showLegacyPlayer = false
|
||||||
|
this.showDashPlayer = true
|
||||||
|
this.videoSourceList = [
|
||||||
|
{
|
||||||
|
url: 'https://invidious.snopyta.org/api/manifest/dash/id/EEIk7gwjgIM',
|
||||||
|
type: 'application/dash+xml',
|
||||||
|
label: 'Dash',
|
||||||
|
qualityLabel: 'Auto'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
this.videoSourceList = result.player_response.streamingData.formats
|
||||||
|
}
|
||||||
|
|
||||||
// The response provides a storyboard, however it returns a 403 error.
|
// The response provides a storyboard, however it returns a 403 error.
|
||||||
// Uncomment this line if that ever changes.
|
// Uncomment this line if that ever changes.
|
||||||
|
@ -275,6 +300,22 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkIfPlaylist: function () {
|
||||||
|
if (typeof (this.$route.query) !== 'undefined') {
|
||||||
|
console.log('defined')
|
||||||
|
console.log(this.$route.query)
|
||||||
|
this.playlistId = this.$route.query.playlistId
|
||||||
|
|
||||||
|
if (typeof (this.playlistId) !== 'undefined') {
|
||||||
|
this.watchingPlaylist = true
|
||||||
|
} else {
|
||||||
|
this.watchingPlaylist = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.watchingPlaylist = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getLegacyFormats: function () {
|
getLegacyFormats: function () {
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('ytGetVideoInformation', this.videoId)
|
.dispatch('ytGetVideoInformation', this.videoId)
|
||||||
|
@ -309,6 +350,15 @@ export default Vue.extend({
|
||||||
}, 100)
|
}, 100)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleVideoEnded: function () {
|
||||||
|
if (this.watchingPlaylist) {
|
||||||
|
console.log('Playlist next video in 5 seconds')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.watchVideoPlaylist.playNextVideo()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleVideoError: function(error) {
|
handleVideoError: function(error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
if (error.code === 4) {
|
if (error.code === 4) {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
class="videoPlayer"
|
class="videoPlayer"
|
||||||
:class="{ theatrePlayer: useTheatreMode }"
|
:class="{ theatrePlayer: useTheatreMode }"
|
||||||
ref="videoPlayer"
|
ref="videoPlayer"
|
||||||
|
@ended="handleVideoEnded"
|
||||||
@error="handleVideoError"
|
@error="handleVideoError"
|
||||||
/>
|
/>
|
||||||
<watch-video-info
|
<watch-video-info
|
||||||
|
@ -45,11 +46,24 @@
|
||||||
class="watchVideo"
|
class="watchVideo"
|
||||||
:class="{ theatreWatchVideo: useTheatreMode }"
|
:class="{ theatreWatchVideo: useTheatreMode }"
|
||||||
/>
|
/>
|
||||||
|
<watch-video-playlist
|
||||||
|
v-if="watchingPlaylist"
|
||||||
|
v-show="!isLoading"
|
||||||
|
:playlist-id="playlistId"
|
||||||
|
:video-id="videoId"
|
||||||
|
ref="watchVideoPlaylist"
|
||||||
|
class="watchVideoSideBar watchVideoPlaylist"
|
||||||
|
:class="{ theatrePlaylist: useTheatreMode }"
|
||||||
|
/>
|
||||||
<watch-video-recommendations
|
<watch-video-recommendations
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
:data="recommendedVideos"
|
:data="recommendedVideos"
|
||||||
class="watchVideoRecommendations"
|
class="watchVideoSideBar watchVideoRecommendations"
|
||||||
:class="{ theatreRecommendations: useTheatreMode }"
|
:class="{
|
||||||
|
theatreRecommendations: useTheatreMode,
|
||||||
|
watchVideoRecommendationsLowerCard: watchingPlaylist,
|
||||||
|
watchVideoRecommendationsNoCard: !watchingPlaylist
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue