Merge pull request #14 from Aasim-A/xml2vtt

Added yt-xml2vtt and did the corresponding implementation
This commit is contained in:
Preston 2020-02-28 18:44:24 -05:00 committed by GitHub
commit f429e346cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 3615 additions and 144 deletions

View File

@ -33,10 +33,12 @@ module.exports = {
plugins: ['vue'], plugins: ['vue'],
rules: { rules: {
'vue/no-v-html': "off", 'space-before-function-paren': 0,
'comma-dangle': 0,
'vue/no-v-html': 'off',
'no-console': 0, 'no-console': 0,
'no-unused-vars': 1, 'no-unused-vars': 1,
'no-undef': 1, 'no-undef': 1,
'vue/no-template-key': 1 'vue/no-template-key': 1,
}, },
} }

3487
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@
"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-xml2srt": "^1.1.0", "yt-xml2vtt": "^1.0.0",
"ytdl-core": "^2.0.0", "ytdl-core": "^2.0.0",
"ytpl": "^0.1.20", "ytpl": "^0.1.20",
"ytsr": "^0.1.10" "ytsr": "^0.1.10"
@ -73,6 +73,7 @@
"fast-glob": "^3.2.2", "fast-glob": "^3.2.2",
"file-loader": "^5.1.0", "file-loader": "^5.1.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^25.1.0",
"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",

View File

@ -1,4 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import xml2vtt from 'yt-xml2vtt'
import $ from 'jquery'
import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
@ -18,9 +20,9 @@ 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-recommendations': WatchVideoRecommendations 'watch-video-recommendations': WatchVideoRecommendations,
}, },
data: function () { data: function() {
return { return {
isLoading: false, isLoading: false,
firstLoad: true, firstLoad: true,
@ -47,44 +49,46 @@ export default Vue.extend({
audioUrl: '', audioUrl: '',
videoSourceList: [], videoSourceList: [],
captionSourceList: [], captionSourceList: [],
recommendedVideos: [] recommendedVideos: [],
} }
}, },
computed: { computed: {
backendPreference: function () { backendPreference: function() {
return this.$store.getters.getBackendPreference return this.$store.getters.getBackendPreference
}, },
backendFallback: function () { backendFallback: function() {
return this.$store.getters.getBackendFallback return this.$store.getters.getBackendFallback
}, },
invidiousInstance: function () { invidiousInstance: function() {
return this.$store.getters.getInvidiousInstance return this.$store.getters.getInvidiousInstance
}, },
videoFormatPreference: function () { videoFormatPreference: function() {
return this.$store.getters.getVideoFormatPreference return this.$store.getters.getVideoFormatPreference
}, },
videoDashUrl: function () { videoDashUrl: function() {
return `${this.invidiousInstance}/api/manifest/dash/id/${this.videoId}.mpd` return `${this.invidiousInstance}/api/manifest/dash/id/${this.videoId}.mpd`
}, },
youtubeNoCookieEmbeddedFrame: function () { youtubeNoCookieEmbeddedFrame: function() {
return `<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>` return `<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>`
}, },
dashSrc: function () { dashSrc: function() {
return [{ return [
url: `${this.invidiousInstance}/api/manifest/dash/${this.videoId}.mpd`, {
type: 'application/dash+xml', url: `${this.invidiousInstance}/api/manifest/dash/${this.videoId}.mpd`,
label: 'Dash' type: 'application/dash+xml',
}] label: 'Dash',
} },
]
},
}, },
watch: { watch: {
$route () { $route() {
// react to route changes... // react to route changes...
this.videoId = this.$route.params.id this.videoId = this.$route.params.id
@ -98,9 +102,9 @@ export default Vue.extend({
this.getVideoInformationInvidious(this.videoId) this.getVideoInformationInvidious(this.videoId)
break break
} }
} },
}, },
mounted: function () { mounted: function() {
this.videoId = this.$route.params.id this.videoId = this.$route.params.id
this.videoStoryboardSrc = `${this.invidiousInstance}/api/v1/storyboards/${this.videoId}?height=90` this.videoStoryboardSrc = `${this.invidiousInstance}/api/v1/storyboards/${this.videoId}?height=90`
@ -120,125 +124,130 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
toggleTheatreMode: function () { toggleTheatreMode: function() {
this.useTheatreMode = !this.useTheatreMode this.useTheatreMode = !this.useTheatreMode
}, },
getVideoInformationLocal: function () { getVideoInformationLocal: function() {
if (this.firstLoad) { if (this.firstLoad) {
this.isLoading = true this.isLoading = true
} }
this.$store.dispatch('ytGetVideoInformation', this.videoId).then((result) => { this.$store
console.log(result) .dispatch('ytGetVideoInformation', this.videoId)
.then(result => {
this.videoTitle = result.title
this.videoViewCount = parseInt(
result.player_response.videoDetails.viewCount,
10
)
this.channelId = result.author.id
this.channelName = result.author.name
this.channelThumbnail = result.author.avatar
this.videoPublished = result.published
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.videoTitle = result.title // The response provides a storyboard, however it returns a 403 error.
this.videoViewCount = parseInt(result.player_response.videoDetails.viewCount) // Uncomment this line if that ever changes.
this.channelId = result.author.id // this.videoStoryboardSrc = result.player_response.storyboards.playerStoryboardSpecRenderer.spec
this.channelName = result.author.name
this.channelThumbnail = result.author.avatar
this.videoPublished = result.published
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
// The response provides a storyboard, however it returns a 403 error. this.captionSourceList =
// Uncomment this line if that ever changes. result.player_response.captions &&
// this.videoStoryboardSrc = result.player_response.storyboards.playerStoryboardSpecRenderer.spec result.player_response.captions.playerCaptionsTracklistRenderer
.captionTracks
this.captionSourceList = result.player_response.captions.playerCaptionsTracklistRenderer.captionTracks if (typeof this.captionSourceList !== 'undefined') {
this.captionSourceList = this.captionSourceList.map(caption => {
caption.type = 'text/vtt'
caption.charset = 'charset=utf-8'
caption.dataSource = 'local'
if (typeof (this.captionSourceList) !== 'undefined') { $.get(caption.baseUrl, response => {
this.captionSourceList = this.captionSourceList.map((caption) => { xml2vtt
caption.baseUrl = `${this.invidiousInstance}/api/v1/captions/${this.videoId}?label=${encodeURI(caption.name.simpleText)}` .Parse(new XMLSerializer().serializeToString(response))
.then(vtt => {
caption.baseUrl = `data:${caption.type};${caption.charset},${vtt}`
})
.catch(err =>
console.log(`Error while converting XML to VTT : ${err}`)
)
}).fail((xhr, textStatus, error) => {
console.log(xhr)
console.log(textStatus)
console.log(error)
})
return caption
})
}
this.isLoading = false
})
.catch(err => {
console.log(err)
if (this.backendPreference === 'local' && this.backendFallback) {
console.log(
'Error getting data with local backend, falling back to Invidious'
)
this.getVideoInformationInvidious()
} else {
this.isLoading = false
// TODO: Show toast with error message
}
})
},
getVideoInformationInvidious: function() {
if (this.firstLoad) {
this.isLoading = true
}
this.$store
.dispatch('invidiousGetVideoInformation', this.videoId)
.then(result => {
console.log(result)
this.videoTitle = result.title
this.videoViewCount = result.viewCount
this.videoLikeCount = result.likeCount
this.videoDislikeCount = result.dislikeCount
this.channelSubscriptionCountText = result.subCountText
this.channelId = result.authorId
this.channelName = result.author
this.channelThumbnail = result.authorThumbnails[1].url
this.videoPublished = result.published * 1000
this.videoDescriptionHtml = result.descriptionHtml
this.recommendedVideos = result.recommendedVideos
this.videoSourceList = result.formatStreams.reverse()
this.captionSourceList = result.captions.map(caption => {
caption.url = this.invidiousInstance + caption.url
caption.type = ''
caption.dataSource = 'invidious'
return caption return caption
}) })
}
// TODO: The response returns the captions of the video, however they're returned
// in XML / TTML. I haven't found a way to properly convert this for use.
// There may be another URL that we can use to grab an appropriate format as well.
// Video.js requires that the captions are returned in .vtt format. The below code
// Converts it to .srt which may work, but I can't get the player to accept the data.
// this.captionSourceList = this.captionSourceList.map((caption) => {
// caption.type = 'application/ttml+xml'
// caption.dataSource = 'local'
//
// $.get(caption.baseUrl, (response) => {
// console.log('response')
// console.log(response)
// console.log()
// xml2srt.Parse(new XMLSerializer().serializeToString(response))
// .then(srt => {
// caption.track = srt
// }).catch(err => console.log(`Error while converting XML to SRT : ${err}`))
// }).fail((xhr, textStatus, error) => {
// console.log(xhr)
// console.log(textStatus)
// console.log(error)
// })
//
// return caption
// })
this.isLoading = false
}).catch((err) => {
console.log(err)
if (this.backendPreference === 'local' && this.backendFallback) {
console.log('Error getting data with local backend, falling back to Invidious')
this.getVideoInformationInvidious()
} else {
this.isLoading = false this.isLoading = false
// TODO: Show toast with error message
}
})
},
getVideoInformationInvidious: function () {
if (this.firstLoad) {
this.isLoading = true
}
this.$store.dispatch('invidiousGetVideoInformation', this.videoId).then((result) => {
console.log(result)
this.videoTitle = result.title
this.videoViewCount = result.viewCount
this.videoLikeCount = result.likeCount
this.videoDislikeCount = result.dislikeCount
this.channelSubscriptionCountText = result.subCountText
this.channelId = result.authorId
this.channelName = result.author
this.channelThumbnail = result.authorThumbnails[1].url
this.videoPublished = result.published * 1000
this.videoDescriptionHtml = result.descriptionHtml
this.recommendedVideos = result.recommendedVideos
this.videoSourceList = result.formatStreams.reverse()
this.captionSourceList = result.captions.map((caption) => {
caption.url = this.invidiousInstance + caption.url
caption.type = ''
caption.dataSource = 'invidious'
return caption
}) })
.catch(err => {
this.isLoading = false console.log(err)
}).catch((err) => { if (this.backendPreference === 'invidious' && this.backendFallback) {
console.log(err) console.log(
if (this.backendPreference === 'invidious' && this.backendFallback) { 'Error getting data with Invidious, falling back to local backend'
console.log('Error getting data with Invidious, falling back to local backend') )
this.getVideoInformationLocal() this.getVideoInformationLocal()
} else { } else {
this.isLoading = false this.isLoading = false
// TODO: Show toast with error message // TODO: Show toast with error message
} }
}) })
}, },
enableDashFormat: function () { enableDashFormat: function() {
if (this.activeFormat === 'dash') { if (this.activeFormat === 'dash') {
return return
} }
@ -246,10 +255,12 @@ export default Vue.extend({
this.activeFormat = 'dash' this.activeFormat = 'dash'
this.hidePlayer = true this.hidePlayer = true
setTimeout(() => { this.hidePlayer = false }, 100) setTimeout(() => {
this.hidePlayer = false
}, 100)
}, },
enableLegacyFormat: function () { enableLegacyFormat: function() {
if (this.activeFormat === 'legacy') { if (this.activeFormat === 'legacy') {
return return
} }
@ -262,16 +273,18 @@ export default Vue.extend({
}, 100) }, 100)
}, },
handleVideoError: function (error) { handleVideoError: function(error) {
console.log(error) console.log(error)
if (error.code === 4) { if (error.code === 4) {
if (this.activeFormat === 'dash') { if (this.activeFormat === 'dash') {
console.log('Unable to play dash formats. Reverting to legacy formats...') console.log(
'Unable to play dash formats. Reverting to legacy formats...'
)
this.enableLegacyFormat() this.enableLegacyFormat()
} else { } else {
this.enableDashFormat() this.enableDashFormat()
} }
} }
} },
} },
}) })