Add full playlist functionality (Shuffle, loop, autoplay)

This commit is contained in:
Preston 2020-05-17 16:12:58 -04:00
parent 1faa075a7b
commit 8980dc74d2
24 changed files with 3094 additions and 1761 deletions

View File

@ -34,9 +34,9 @@ At this time, here is the list of things to do/need to do:
- [x] Playlist View
- [x] Video Watch Page (Recommendations, Comments)
- [ ] Video player logic (Switching formats / quality, live video, fallback logic)
- [ ] Playlist logic (Autoplay next video, shuffle list)
- [ ] Database Setup and Logic (Updating and creating data)
- [ ] Settings Page
- [x] Playlist logic (Autoplay next video, shuffle list)
- [x] Database Setup and Logic (Updating and creating data)
- [x] Settings Page
- [ ] Subscriptions Page and Logic
- [ ] Playlists Page (Will allow for creating user playlists. Will replace the "Favorites" Page)
- [ ] History Page and Logic

3836
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,18 +11,18 @@
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@silvermine/videojs-quality-selector": "^1.2.3",
"@silvermine/videojs-quality-selector": "^1.2.4",
"autolinker": "^3.14.1",
"bulma-pro": "^0.1.8",
"bulma-pro": "^0.2.0",
"dateformat": "^3.0.3",
"electron-context-menu": "^1.0.0",
"jquery": "^3.5.0",
"electron-context-menu": "^2.0.1",
"jquery": "^3.5.1",
"lodash.isequal": "^4.5.0",
"material-design-icons": "^3.0.1",
"mediaelement": "^4.2.16",
"nedb": "^1.8.0",
"opml-to-json": "0.0.3",
"video.js": "^7.7.5",
"video.js": "^7.7.6",
"videojs-abloop": "^1.1.2",
"videojs-contrib-quality-levels": "^2.0.9",
"videojs-http-source-selector": "^1.1.6",
@ -31,38 +31,38 @@
"vue": "^2.6.11",
"vue-electron": "^1.0.6",
"vue-router": "^3.1.6",
"vuex": "^3.2.0",
"vuex": "^3.4.0",
"xml2json": "^0.12.0",
"youtube-chat": "^1.0.2",
"youtube-chat": "^1.1.0",
"youtube-comments-fetch": "^1.0.1",
"youtube-comments-task": "^1.3.14",
"youtube-suggest": "^1.1.0",
"yt-xml2vtt": "^1.0.1",
"ytdl-core": "^2.1.0",
"ytpl": "^0.1.20",
"ytsr": "^0.1.12"
"ytdl-core": "^2.1.2",
"ytpl": "^0.1.21",
"ytsr": "^0.1.13"
},
"description": "A private YouTube client",
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
"@babel/preset-env": "^7.9.5",
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-typescript": "^7.9.0",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"@typescript-eslint/parser": "^2.29.0",
"acorn": "^7.1.1",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"acorn": "^7.2.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.5.2",
"copy-webpack-plugin": "^6.0.1",
"css-loader": "^3.5.3",
"devtron": "^1.4.0",
"electron": "^8.2.3",
"electron-builder": "^22.5.1",
"electron": "^8.3.0",
"electron-builder": "^22.6.0",
"electron-debug": "^3.0.1",
"electron-rebuild": "^1.10.1",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"electron-rebuild": "^1.11.0",
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
@ -72,26 +72,26 @@
"eslint-plugin-vue": "^6.2.2",
"fast-glob": "^3.2.2",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"jest": "^25.4.0",
"html-webpack-plugin": "^4.3.0",
"jest": "^26.0.1",
"mini-css-extract-plugin": "^0.9.0",
"node-loader": "^0.6.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.4",
"sass": "^1.26.3",
"prettier": "^2.0.5",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"style-loader": "^1.1.4",
"style-loader": "^1.2.1",
"tree-kill": "1.2.2",
"typescript": "^3.8.3",
"typescript": "^3.9.2",
"url-loader": "^4.1.0",
"vue-devtools": "^5.1.3",
"vue-eslint-parser": "^7.0.0",
"vue-loader": "^15.9.1",
"vue-eslint-parser": "^7.1.0",
"vue-loader": "^15.9.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
"webpack-dev-server": "^3.11.0"
},
"license": "GPL-3.0-or-later",
"main": "./dist/main.js",

View File

@ -7,6 +7,10 @@ export default Vue.extend({
type: Object,
required: true
},
playlistId: {
type: String,
default: null
},
forceListType: {
type: String,
default: null
@ -16,7 +20,6 @@ export default Vue.extend({
return {
id: '',
title: '',
thumbnail: '',
channelName: '',
channelId: '',
viewCount: 0,
@ -37,6 +40,19 @@ export default Vue.extend({
thumbnailPreference: function () {
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 () {
@ -54,11 +70,27 @@ export default Vue.extend({
},
methods: {
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}` })
}
},
goToChannel: function () {
console.log(this.data)
this.$router.push({ path: `/channel/${this.channelId}` })
},
@ -101,20 +133,7 @@ export default Vue.extend({
this.id = this.data.videoId
this.title = this.data.title
// 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.channelId = this.data.authorId
this.duration = this.calculateVideoDuration(this.data.lengthSeconds)
@ -142,22 +161,6 @@ export default Vue.extend({
}
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') {
this.channelName = this.data.author
@ -188,7 +191,7 @@ export default Vue.extend({
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(' ')
this.viewCount = parseInt(uploadSplit[0])
this.isLive = true

View File

@ -37,7 +37,10 @@
:style="{width: progressPercentage + '%'}"
/>
</div>
<p class="videoTitle">
<p
class="videoTitle"
@click="play(id)"
>
{{ title }}
</p>
<p
@ -49,30 +52,35 @@
<span
v-if="!isLive && !hideViews"
class="viewCount"
@click="play(id)"
>
{{ viewCount }} views
</span>
<span
v-if="uploadedTime !== '' && !isLive"
class="uploadedTime"
@click="play(id)"
>
- {{ uploadedTime }}
</span>
<span
v-if="isLive"
class="viewCount"
@click="play(id)"
>
{{ viewCount }} watching
</span>
<p
v-if="listType !== 'grid'"
class="description"
@click="play(id)"
>
{{ description }}
</p>
<span
v-if="isLive"
class="liveText"
@click="play(id)"
>
LIVE NOW
</span>

View File

@ -100,12 +100,80 @@ export default Vue.extend({
return this.$store.getters.getDefaultPlayback
},
defaultQuality: function () {
return this.$store.getters.getDefaultQuality
},
defaultVideoFormat: function () {
return this.$store.getters.getDefaultVideoFormat
},
autoplayVideos: function () {
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: {
@ -172,6 +240,10 @@ export default Vue.extend({
const v = this
this.player.on('ended', function () {
v.$emit('ended')
})
this.player.on('error', function (error, message) {
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 () {
if (this.dashSrc === null) {
console.log('No dash format available.')

View File

@ -13,6 +13,7 @@
:src="source.url"
:type="source.type || source.mimeType"
:label="source.qualityLabel"
:selected="source.qualityLabel === selectedDefaultQuality"
/>
<track
v-for="(caption, index) in captionList"

View File

@ -42,15 +42,15 @@ export default Vue.extend({
],
qualityValues: [
'auto',
'144',
'240',
'360',
'480',
'720',
'1080',
'1440',
'4k',
'8k'
144,
240,
360,
480,
720,
1080,
1440,
2160,
4320
]
}
},

View File

@ -16,8 +16,8 @@ export default Vue.extend({
data: function () {
return {
id: '',
randomVideoId: '',
title: '',
thumbnail: '',
channelThumbnail: '',
channelName: '',
channelId: '',
@ -25,6 +25,7 @@ export default Vue.extend({
viewCount: 0,
lastUpdated: '',
description: '',
infoSource: '',
shareHeaders: [
'Copy YouTube Link',
'Open in YouTube',
@ -42,17 +43,35 @@ export default Vue.extend({
computed: {
listType: function () {
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 () {
console.log(this.data)
this.id = this.data.id
this.randomVideoId = this.data.randomVideoId
this.title = this.data.title
this.thumbnail = this.data.thumbnail
this.channelName = this.data.channelName
this.channelThumbnail = this.data.channelThumbnail
this.uploadedTime = this.data.uploaded_at
this.description = this.data.description
this.infoSource = this.data.infoSource
// Causes errors if not put inside of a check
if (typeof (this.data.viewCount) !== 'undefined') {

View File

@ -11,7 +11,11 @@
{{ title }}
</h2>
<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>
{{ description }}

View File

@ -44,11 +44,11 @@ export default Vue.extend({
},
likeCount: {
type: Number,
required: true
default: 0
},
dislikeCount: {
type: Number,
required: true
default: 0
}
},
data: function () {
@ -88,6 +88,10 @@ export default Vue.extend({
return this.$store.getters.getInvidiousInstance
},
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
invidiousUrl: function () {
return `${this.invidiousInstance}/watch?v=${this.id}`
},
@ -118,7 +122,7 @@ export default Vue.extend({
},
methods: {
goToChannel: function () {
console.log('TODO: Handle goToChannel')
this.$router.push({ path: `/channel/${this.channelId}` })
},
handleSubscription: function () {
@ -126,9 +130,6 @@ export default Vue.extend({
},
handleFormatChange: function (format) {
console.log('Handling share')
console.log(this)
switch (format) {
case 'dash':
this.$parent.enableDashFormat()
@ -147,19 +148,28 @@ export default Vue.extend({
navigator.clipboard.writeText(this.youtubeUrl)
break
case 'openYoutube':
// shell.openExternal(this.youtubeUrl)
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.youtubeUrl)
}
break
case 'copyYoutubeEmbed':
navigator.clipboard.writeText(this.youtubeEmbedUrl)
break
case 'openYoutubeEmbed':
// shell.openExternal(this.youtubeEmbedUrl)
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.youtubeEmbedUrl)
}
break
case 'copyInvidious':
navigator.clipboard.writeText(this.invidiousUrl)
break
case 'openInvidious':
// shell.openExternal(this.invidiousUrl)
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.invidiousUrl)
}
break
}
}

View File

@ -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;
}

View File

@ -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
}
})
}
}
})

View File

@ -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" />

View File

@ -5,3 +5,29 @@
.videoRecommendation {
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;
}

View File

@ -14,11 +14,6 @@ export default Vue.extend({
required: true
}
},
data: function () {
return {
test: 'hello'
}
},
computed: {
listType: function () {
return this.$store.getters.getListType

View File

@ -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
}

View File

@ -118,7 +118,7 @@ const actions = {
return new Promise((resolve, reject) => {
console.log(playlistId)
console.log('Getting playlist info please wait...')
ytpl(playlistId, (err, result) => {
ytpl(playlistId, { limit: 0 }, (err, result) => {
if (err) {
reject(err)
} else {

View File

@ -1,7 +1,7 @@
@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 {
position: absolute;
top: 3px;
top: 0px;
left: 0;
width: 100%;
height: 100%;

View File

@ -18,7 +18,7 @@ export default Vue.extend({
data: function () {
return {
isLoading: false,
playlistId: '',
playlistId: null,
nextPageRef: '',
lastSearchQuery: '',
playlistPage: 1,
@ -82,13 +82,14 @@ export default Vue.extend({
id: result.id,
title: result.title,
description: result.description,
thumbnail: result.items[randomVideoIndex].thumbnail,
randomVideoId: result.items[randomVideoIndex].id,
viewCount: result.views,
videoCount: result.total_items,
lastUpdated: result.last_updated,
channelName: result.author.name,
channelThumbnail: result.author.avatar,
channelId: result.author.id
channelId: result.author.id,
infoSource: 'local'
}
this.playlistItems = result.items
@ -127,12 +128,13 @@ export default Vue.extend({
id: result.playlistId,
title: result.title,
description: result.description,
thumbnail: result.videos[randomVideoIndex].videoThumbnails[0].url,
randomVideoId: result.videos[randomVideoIndex].videoId,
viewCount: result.viewCount,
videoCount: result.videoCount,
channelName: result.author,
channelThumbnail: result.authorThumbnails[2].url,
channelId: result.authorId
channelId: result.authorId,
infoSource: 'invidious'
}
const dateString = new Date(result.updated * 1000)

View File

@ -9,18 +9,21 @@
:data="infoData"
class="playlistInfo"
/>
<ft-flex-box
<ft-card
v-if="!isLoading"
class="playlistItems"
>
<ft-flex-box>
<ft-list-video
v-for="(item, index) in playlistItems"
:key="index"
:data="item"
:playlist-id="playlistId"
force-list-type="list"
class="playlistItem"
/>
</ft-flex-box>
</ft-card>
</div>
</template>

View File

@ -27,14 +27,40 @@
margin-bottom: 10px;
}
.watchVideoRecommendations {
.watchVideoSideBar {
width: 27%;
max-width: 425px;
float: right;
margin-bottom: 10px;
position: absolute;
top: 70px;
}
.watchVideoPlaylist {
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 {
@ -78,6 +104,14 @@
margin-bottom: 10px;
}
.watchVideoPlaylist {
float: none;
margin: 0 auto;
width: 85%;
max-width: none;
position: static;
}
.watchVideoRecommendations {
float: none;
margin: 0 auto;

View File

@ -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 WatchVideoDescription from '../../components/watch-video-description/watch-video-description.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'
export default Vue.extend({
@ -20,6 +21,7 @@ export default Vue.extend({
'watch-video-info': WatchVideoInfo,
'watch-video-description': WatchVideoDescription,
'watch-video-comments': WatchVideoComments,
'watch-video-playlist': WatchVideoPlaylist,
'watch-video-recommendations': WatchVideoRecommendations,
},
data: function() {
@ -31,6 +33,7 @@ export default Vue.extend({
showLegacyPlayer: false,
showYouTubeNoCookieEmbed: false,
hidePlayer: false,
isLive: false,
activeFormat: 'legacy',
videoId: '',
videoTitle: '',
@ -49,6 +52,8 @@ export default Vue.extend({
videoSourceList: [],
captionSourceList: [],
recommendedVideos: [],
watchingPlaylist: false,
playlistId: '',
}
},
computed: {
@ -111,6 +116,8 @@ export default Vue.extend({
this.firstLoad = true
this.checkIfPlaylist()
switch (this.backendPreference) {
case 'local':
this.getVideoInformationLocal(this.videoId)
@ -132,6 +139,8 @@ export default Vue.extend({
this.activeFormat = this.defaultVideoFormat
this.useTheatreMode = this.defaultTheatreMode
this.checkIfPlaylist()
if (!this.usingElectron) {
this.getVideoInformationInvidious()
} else {
@ -158,6 +167,7 @@ export default Vue.extend({
this.$store
.dispatch('ytGetVideoInformation', this.videoId)
.then(result => {
console.log(result)
this.videoTitle = result.title
this.videoViewCount = parseInt(
result.player_response.videoDetails.viewCount,
@ -170,9 +180,24 @@ export default Vue.extend({
this.videoDescription =
result.player_response.videoDetails.shortDescription
this.recommendedVideos = result.related_videos
this.videoSourceList = result.player_response.streamingData.formats
this.videoLikeCount = result.likes
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.
// 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 () {
this.$store
.dispatch('ytGetVideoInformation', this.videoId)
@ -309,6 +350,15 @@ export default Vue.extend({
}, 100)
},
handleVideoEnded: function () {
if (this.watchingPlaylist) {
console.log('Playlist next video in 5 seconds')
setTimeout(() => {
this.$refs.watchVideoPlaylist.playNextVideo()
}, 5000)
}
},
handleVideoError: function(error) {
console.log(error)
if (error.code === 4) {

View File

@ -14,6 +14,7 @@
class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }"
ref="videoPlayer"
@ended="handleVideoEnded"
@error="handleVideoError"
/>
<watch-video-info
@ -45,11 +46,24 @@
class="watchVideo"
: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
v-if="!isLoading"
:data="recommendedVideos"
class="watchVideoRecommendations"
:class="{ theatreRecommendations: useTheatreMode }"
class="watchVideoSideBar watchVideoRecommendations"
:class="{
theatreRecommendations: useTheatreMode,
watchVideoRecommendationsLowerCard: watchingPlaylist,
watchVideoRecommendationsNoCard: !watchingPlaylist
}"
/>
</div>
</template>