2020-02-16 19:30:00 +01:00
|
|
|
|
import Vue from 'vue'
|
2020-08-05 05:44:34 +02:00
|
|
|
|
import { mapActions } from 'vuex'
|
2022-12-29 02:19:48 +01:00
|
|
|
|
import fs from 'fs/promises'
|
2020-08-28 21:43:10 +02:00
|
|
|
|
import ytDashGen from 'yt-dash-manifest-generator'
|
2020-02-16 19:30:00 +01:00
|
|
|
|
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
|
|
|
|
import FtVideoPlayer from '../../components/ft-video-player/ft-video-player.vue'
|
|
|
|
|
import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue'
|
2022-09-29 22:01:54 +02:00
|
|
|
|
import WatchVideoChapters from '../../components/watch-video-chapters/watch-video-chapters.vue'
|
2020-02-16 19:30:00 +01:00
|
|
|
|
import WatchVideoDescription from '../../components/watch-video-description/watch-video-description.vue'
|
|
|
|
|
import WatchVideoComments from '../../components/watch-video-comments/watch-video-comments.vue'
|
2020-05-23 23:29:42 +02:00
|
|
|
|
import WatchVideoLiveChat from '../../components/watch-video-live-chat/watch-video-live-chat.vue'
|
2020-05-17 22:12:58 +02:00
|
|
|
|
import WatchVideoPlaylist from '../../components/watch-video-playlist/watch-video-playlist.vue'
|
2020-02-16 19:30:00 +01:00
|
|
|
|
import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue'
|
2022-06-21 08:14:15 +02:00
|
|
|
|
import FtAgeRestricted from '../../components/ft-age-restricted/ft-age-restricted.vue'
|
2022-09-19 14:14:53 +02:00
|
|
|
|
import i18n from '../../i18n/index'
|
2022-12-29 02:19:48 +01:00
|
|
|
|
import { pathExists } from '../../helpers/filesystem'
|
2022-11-04 08:19:51 +01:00
|
|
|
|
import {
|
|
|
|
|
buildVTTFileLocally,
|
|
|
|
|
copyToClipboard,
|
|
|
|
|
formatDurationAsTimestamp,
|
2022-12-06 12:37:44 +01:00
|
|
|
|
getUserDataPath,
|
2022-11-04 08:19:51 +01:00
|
|
|
|
showToast
|
|
|
|
|
} from '../../helpers/utils'
|
2020-02-16 19:30:00 +01:00
|
|
|
|
|
|
|
|
|
export default Vue.extend({
|
|
|
|
|
name: 'Watch',
|
|
|
|
|
components: {
|
|
|
|
|
'ft-loader': FtLoader,
|
|
|
|
|
'ft-video-player': FtVideoPlayer,
|
|
|
|
|
'watch-video-info': WatchVideoInfo,
|
2022-09-29 22:01:54 +02:00
|
|
|
|
'watch-video-chapters': WatchVideoChapters,
|
2020-02-16 19:30:00 +01:00
|
|
|
|
'watch-video-description': WatchVideoDescription,
|
|
|
|
|
'watch-video-comments': WatchVideoComments,
|
2020-05-23 23:29:42 +02:00
|
|
|
|
'watch-video-live-chat': WatchVideoLiveChat,
|
2020-05-17 22:12:58 +02:00
|
|
|
|
'watch-video-playlist': WatchVideoPlaylist,
|
2022-06-21 08:14:15 +02:00
|
|
|
|
'watch-video-recommendations': WatchVideoRecommendations,
|
|
|
|
|
'ft-age-restricted': FtAgeRestricted
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
2020-10-04 20:42:46 +02:00
|
|
|
|
beforeRouteLeave: function (to, from, next) {
|
2022-06-21 04:33:42 +02:00
|
|
|
|
this.handleRouteChange(this.videoId)
|
2021-04-21 20:38:46 +02:00
|
|
|
|
window.removeEventListener('beforeunload', this.handleWatchProgress)
|
2020-10-04 20:42:46 +02:00
|
|
|
|
next()
|
|
|
|
|
},
|
2022-06-21 04:33:42 +02:00
|
|
|
|
data: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
return {
|
2023-01-02 03:03:21 +01:00
|
|
|
|
isLoading: true,
|
2020-02-16 19:30:00 +01:00
|
|
|
|
firstLoad: true,
|
2020-04-22 04:59:09 +02:00
|
|
|
|
useTheatreMode: false,
|
2020-02-16 19:30:00 +01:00
|
|
|
|
showDashPlayer: true,
|
|
|
|
|
showLegacyPlayer: false,
|
|
|
|
|
showYouTubeNoCookieEmbed: false,
|
2020-02-19 04:31:10 +01:00
|
|
|
|
hidePlayer: false,
|
2022-06-21 08:14:15 +02:00
|
|
|
|
isFamilyFriendly: false,
|
2020-05-17 22:12:58 +02:00
|
|
|
|
isLive: false,
|
2020-10-31 15:57:51 +01:00
|
|
|
|
isLiveContent: false,
|
2020-09-16 04:07:54 +02:00
|
|
|
|
isUpcoming: false,
|
|
|
|
|
upcomingTimestamp: null,
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
upcomingTimeLeft: null,
|
2020-02-19 04:31:10 +01:00
|
|
|
|
activeFormat: 'legacy',
|
2020-09-22 03:30:48 +02:00
|
|
|
|
thumbnail: '',
|
2020-02-16 19:30:00 +01:00
|
|
|
|
videoId: '',
|
|
|
|
|
videoTitle: '',
|
|
|
|
|
videoDescription: '',
|
|
|
|
|
videoDescriptionHtml: '',
|
|
|
|
|
videoViewCount: 0,
|
|
|
|
|
videoLikeCount: 0,
|
|
|
|
|
videoDislikeCount: 0,
|
2020-08-20 04:39:44 +02:00
|
|
|
|
videoLengthSeconds: 0,
|
2022-09-29 22:01:54 +02:00
|
|
|
|
videoChapters: [],
|
|
|
|
|
videoCurrentChapterIndex: 0,
|
2020-02-16 19:30:00 +01:00
|
|
|
|
channelName: '',
|
|
|
|
|
channelThumbnail: '',
|
|
|
|
|
channelId: '',
|
|
|
|
|
channelSubscriptionCountText: '',
|
|
|
|
|
videoPublished: 0,
|
2020-02-18 21:59:01 +01:00
|
|
|
|
videoStoryboardSrc: '',
|
2020-02-16 19:30:00 +01:00
|
|
|
|
audioUrl: '',
|
2020-08-28 21:43:10 +02:00
|
|
|
|
dashSrc: [],
|
2020-06-01 04:47:22 +02:00
|
|
|
|
activeSourceList: [],
|
2020-02-18 21:59:01 +01:00
|
|
|
|
videoSourceList: [],
|
2020-06-01 04:47:22 +02:00
|
|
|
|
audioSourceList: [],
|
2021-05-15 21:08:41 +02:00
|
|
|
|
adaptiveFormats: [],
|
2021-03-17 02:28:25 +01:00
|
|
|
|
captionHybridList: [], // [] -> Promise[] -> string[] (URIs)
|
2020-02-28 04:29:30 +01:00
|
|
|
|
recommendedVideos: [],
|
2020-10-08 21:01:46 +02:00
|
|
|
|
downloadLinks: [],
|
2020-05-17 22:12:58 +02:00
|
|
|
|
watchingPlaylist: false,
|
2020-09-22 00:39:25 +02:00
|
|
|
|
playlistId: '',
|
2021-03-06 20:03:40 +01:00
|
|
|
|
timestamp: null,
|
2021-06-14 20:10:21 +02:00
|
|
|
|
playNextTimeout: null,
|
2022-05-02 13:50:23 +02:00
|
|
|
|
playNextCountDownIntervalId: null,
|
2022-07-31 16:08:49 +02:00
|
|
|
|
pictureInPictureButtonInverval: null,
|
|
|
|
|
infoAreaSticky: true
|
2020-02-16 19:30:00 +01:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
2020-08-20 04:39:44 +02:00
|
|
|
|
historyCache: function () {
|
|
|
|
|
return this.$store.getters.getHistoryCache
|
|
|
|
|
},
|
|
|
|
|
rememberHistory: function () {
|
|
|
|
|
return this.$store.getters.getRememberHistory
|
|
|
|
|
},
|
2021-04-01 15:54:45 +02:00
|
|
|
|
removeVideoMetaFiles: function () {
|
|
|
|
|
return this.$store.getters.getRemoveVideoMetaFiles
|
|
|
|
|
},
|
2020-08-22 22:51:04 +02:00
|
|
|
|
saveWatchedProgress: function () {
|
|
|
|
|
return this.$store.getters.getSaveWatchedProgress
|
|
|
|
|
},
|
2020-03-01 04:37:02 +01:00
|
|
|
|
backendPreference: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
return this.$store.getters.getBackendPreference
|
|
|
|
|
},
|
2020-03-01 04:37:02 +01:00
|
|
|
|
backendFallback: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
return this.$store.getters.getBackendFallback
|
|
|
|
|
},
|
2021-07-03 03:55:56 +02:00
|
|
|
|
currentInvidiousInstance: function () {
|
|
|
|
|
return this.$store.getters.getCurrentInvidiousInstance
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
2020-04-14 04:59:25 +02:00
|
|
|
|
proxyVideos: function () {
|
|
|
|
|
return this.$store.getters.getProxyVideos
|
|
|
|
|
},
|
2021-03-06 17:25:50 +01:00
|
|
|
|
defaultInterval: function () {
|
|
|
|
|
return this.$store.getters.getDefaultInterval
|
|
|
|
|
},
|
2020-04-22 04:59:09 +02:00
|
|
|
|
defaultTheatreMode: function () {
|
|
|
|
|
return this.$store.getters.getDefaultTheatreMode
|
|
|
|
|
},
|
2020-03-01 04:37:02 +01:00
|
|
|
|
defaultVideoFormat: function () {
|
|
|
|
|
return this.$store.getters.getDefaultVideoFormat
|
2020-02-19 04:31:10 +01:00
|
|
|
|
},
|
2020-03-01 04:37:02 +01:00
|
|
|
|
forceLocalBackendForLegacy: function () {
|
|
|
|
|
return this.$store.getters.getForceLocalBackendForLegacy
|
|
|
|
|
},
|
2020-06-01 04:47:22 +02:00
|
|
|
|
thumbnailPreference: function () {
|
|
|
|
|
return this.$store.getters.getThumbnailPreference
|
|
|
|
|
},
|
2020-09-07 20:43:44 +02:00
|
|
|
|
playNextVideo: function () {
|
|
|
|
|
return this.$store.getters.getPlayNextVideo
|
|
|
|
|
},
|
2022-07-31 21:37:34 +02:00
|
|
|
|
autoplayPlaylists: function () {
|
|
|
|
|
return this.$store.getters.getAutoplayPlaylists
|
|
|
|
|
},
|
2021-01-13 05:13:36 +01:00
|
|
|
|
hideRecommendedVideos: function () {
|
|
|
|
|
return this.$store.getters.getHideRecommendedVideos
|
|
|
|
|
},
|
|
|
|
|
hideLiveChat: function () {
|
|
|
|
|
return this.$store.getters.getHideLiveChat
|
|
|
|
|
},
|
2022-06-21 08:14:15 +02:00
|
|
|
|
hideComments: function () {
|
|
|
|
|
return this.$store.getters.getHideComments
|
|
|
|
|
},
|
|
|
|
|
hideVideoDescription: function () {
|
|
|
|
|
return this.$store.getters.getHideVideoDescription
|
|
|
|
|
},
|
|
|
|
|
showFamilyFriendlyOnly: function() {
|
|
|
|
|
return this.$store.getters.getShowFamilyFriendlyOnly
|
|
|
|
|
},
|
2020-06-01 04:47:22 +02:00
|
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
|
youtubeNoCookieEmbeddedFrame: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
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>`
|
2020-10-06 04:27:32 +02:00
|
|
|
|
},
|
|
|
|
|
hideChannelSubscriptions: function () {
|
|
|
|
|
return this.$store.getters.getHideChannelSubscriptions
|
|
|
|
|
},
|
|
|
|
|
hideVideoLikesAndDislikes: function () {
|
|
|
|
|
return this.$store.getters.getHideVideoLikesAndDislikes
|
2021-01-13 05:13:36 +01:00
|
|
|
|
},
|
2022-06-21 04:33:42 +02:00
|
|
|
|
theatrePossible: function () {
|
2021-01-13 05:13:36 +01:00
|
|
|
|
return !this.hideRecommendedVideos || (!this.hideLiveChat && this.isLive) || this.watchingPlaylist
|
2022-09-19 14:14:53 +02:00
|
|
|
|
},
|
|
|
|
|
currentLocale: function () {
|
|
|
|
|
return i18n.locale.replace('_', '-')
|
2022-09-29 22:01:54 +02:00
|
|
|
|
},
|
|
|
|
|
hideChapters: function () {
|
|
|
|
|
return this.$store.getters.getHideChapters
|
2020-08-05 04:18:39 +02:00
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
|
|
|
|
watch: {
|
2020-02-28 04:29:30 +01:00
|
|
|
|
$route() {
|
2022-06-21 04:33:42 +02:00
|
|
|
|
this.handleRouteChange(this.videoId)
|
2020-02-16 19:30:00 +01:00
|
|
|
|
// react to route changes...
|
|
|
|
|
this.videoId = this.$route.params.id
|
|
|
|
|
|
|
|
|
|
this.firstLoad = true
|
2020-06-02 04:42:29 +02:00
|
|
|
|
this.activeFormat = this.defaultVideoFormat
|
2020-08-17 00:11:44 +02:00
|
|
|
|
this.videoStoryboardSrc = ''
|
2021-03-17 02:28:25 +01:00
|
|
|
|
this.captionHybridList = []
|
2020-10-08 21:01:46 +02:00
|
|
|
|
this.downloadLinks = []
|
2020-02-16 19:30:00 +01:00
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
|
this.checkIfPlaylist()
|
2021-03-06 20:03:40 +01:00
|
|
|
|
this.checkIfTimestamp()
|
2020-05-17 22:12:58 +02:00
|
|
|
|
|
2020-02-16 19:30:00 +01:00
|
|
|
|
switch (this.backendPreference) {
|
|
|
|
|
case 'local':
|
|
|
|
|
this.getVideoInformationLocal(this.videoId)
|
|
|
|
|
break
|
|
|
|
|
case 'invidious':
|
|
|
|
|
this.getVideoInformationInvidious(this.videoId)
|
2020-03-01 04:37:02 +01:00
|
|
|
|
|
|
|
|
|
if (this.forceLocalBackendForLegacy) {
|
|
|
|
|
this.getVideoInformationLocal(this.videoId)
|
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
|
break
|
|
|
|
|
}
|
2022-05-02 13:50:23 +02:00
|
|
|
|
},
|
|
|
|
|
activeFormat: function (format) {
|
|
|
|
|
clearInterval(this.pictureInPictureButtonInverval)
|
|
|
|
|
|
|
|
|
|
// only hide/show the button once the player is available
|
|
|
|
|
this.pictureInPictureButtonInverval = setInterval(() => {
|
|
|
|
|
if (!this.hidePlayer) {
|
2022-05-15 03:05:14 +02:00
|
|
|
|
const pipButton = document.querySelector('.vjs-picture-in-picture-control')
|
|
|
|
|
if (pipButton === null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-02 13:50:23 +02:00
|
|
|
|
if (format === 'audio') {
|
2022-05-15 03:05:14 +02:00
|
|
|
|
pipButton.classList.add('vjs-hidden')
|
2022-05-02 13:50:23 +02:00
|
|
|
|
} else {
|
2022-05-15 03:05:14 +02:00
|
|
|
|
pipButton.classList.remove('vjs-hidden')
|
2022-05-02 13:50:23 +02:00
|
|
|
|
}
|
|
|
|
|
clearInterval(this.pictureInPictureButtonInverval)
|
|
|
|
|
}
|
|
|
|
|
}, 100)
|
2020-08-05 04:18:39 +02:00
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
2020-05-17 22:12:58 +02:00
|
|
|
|
mounted: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
this.videoId = this.$route.params.id
|
2020-03-01 04:37:02 +01:00
|
|
|
|
this.activeFormat = this.defaultVideoFormat
|
2020-04-22 04:59:09 +02:00
|
|
|
|
this.useTheatreMode = this.defaultTheatreMode
|
2020-02-19 04:31:10 +01:00
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
|
this.checkIfPlaylist()
|
2021-03-06 20:03:40 +01:00
|
|
|
|
this.checkIfTimestamp()
|
2020-05-17 22:12:58 +02:00
|
|
|
|
|
2022-09-17 10:19:31 +02:00
|
|
|
|
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
2020-04-18 05:17:45 +02:00
|
|
|
|
this.getVideoInformationInvidious()
|
|
|
|
|
} else {
|
2022-09-17 10:19:31 +02:00
|
|
|
|
this.getVideoInformationLocal()
|
2020-02-16 19:30:00 +01:00
|
|
|
|
}
|
2021-04-21 20:38:46 +02:00
|
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', this.handleWatchProgress)
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
|
|
|
|
methods: {
|
2022-06-21 04:33:42 +02:00
|
|
|
|
changeTimestamp: function (timestamp) {
|
2020-09-19 18:19:58 +02:00
|
|
|
|
this.$refs.videoPlayer.player.currentTime(timestamp)
|
|
|
|
|
},
|
2022-06-21 04:33:42 +02:00
|
|
|
|
toggleTheatreMode: function () {
|
2020-02-18 21:59:01 +01:00
|
|
|
|
this.useTheatreMode = !this.useTheatreMode
|
|
|
|
|
},
|
|
|
|
|
|
2022-06-21 04:33:42 +02:00
|
|
|
|
getVideoInformationLocal: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
if (this.firstLoad) {
|
|
|
|
|
this.isLoading = true
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-22 01:56:32 +02:00
|
|
|
|
this.ytGetVideoInformation(this.videoId)
|
2020-08-26 22:45:20 +02:00
|
|
|
|
.then(async result => {
|
2020-12-02 19:36:08 +01:00
|
|
|
|
const playabilityStatus = result.player_response.playabilityStatus
|
2020-12-21 03:43:40 +01:00
|
|
|
|
if (playabilityStatus.status === 'UNPLAYABLE') {
|
2020-12-02 19:36:08 +01:00
|
|
|
|
const errorScreen = playabilityStatus.errorScreen.playerErrorMessageRenderer
|
|
|
|
|
const reason = errorScreen.reason.simpleText
|
|
|
|
|
let subReason
|
|
|
|
|
let skipIndex
|
|
|
|
|
errorScreen.subreason.runs.forEach((message, index) => {
|
|
|
|
|
if (index !== skipIndex) {
|
|
|
|
|
if (message.text.match(/<a.*>/)) {
|
|
|
|
|
skipIndex = index + 1
|
|
|
|
|
} else if (!message.text.match(/<\/a>/)) {
|
|
|
|
|
if (typeof subReason === 'undefined') {
|
|
|
|
|
subReason = message.text
|
|
|
|
|
} else {
|
|
|
|
|
subReason = subReason + message.text
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
throw new Error(`${reason}: ${subReason}`)
|
|
|
|
|
}
|
2022-09-09 19:39:56 +02:00
|
|
|
|
try {
|
|
|
|
|
// workaround for title localization
|
2022-09-19 14:49:30 +02:00
|
|
|
|
this.videoTitle = result.response.contents.twoColumnWatchNextResults.results.results.contents[0].videoPrimaryInfoRenderer.title.runs.map(run => run.text).join('')
|
2022-09-09 19:39:56 +02:00
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to extract localised video title, falling back to the standard one.', err)
|
|
|
|
|
// if the workaround for localization fails, this sets the title to the potentially non-localized value
|
|
|
|
|
this.videoTitle = result.videoDetails.title
|
|
|
|
|
}
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.videoViewCount = parseInt(
|
|
|
|
|
result.player_response.videoDetails.viewCount,
|
|
|
|
|
10
|
|
|
|
|
)
|
2020-12-02 19:35:03 +01:00
|
|
|
|
if ('id' in result.videoDetails.author) {
|
2020-12-07 02:18:08 +01:00
|
|
|
|
this.channelId = result.player_response.videoDetails.channelId
|
2020-12-02 19:35:03 +01:00
|
|
|
|
this.channelName = result.videoDetails.author.name
|
2021-01-15 04:37:52 +01:00
|
|
|
|
if (result.videoDetails.author.thumbnails.length > 0) {
|
|
|
|
|
this.channelThumbnail = result.videoDetails.author.thumbnails[0].url
|
|
|
|
|
}
|
2020-12-02 19:35:03 +01:00
|
|
|
|
} else {
|
|
|
|
|
this.channelId = result.player_response.videoDetails.channelId
|
|
|
|
|
this.channelName = result.player_response.videoDetails.author
|
|
|
|
|
this.channelThumbnail = result.player_response.embedPreview.thumbnailPreviewRenderer.videoDetails.embeddedPlayerOverlayVideoDetailsRenderer.channelThumbnail.thumbnails[0].url
|
|
|
|
|
}
|
2022-06-03 14:52:35 +02:00
|
|
|
|
this.updateSubscriptionDetails({
|
|
|
|
|
channelThumbnailUrl: this.channelThumbnail,
|
|
|
|
|
channelName: this.channelName,
|
|
|
|
|
channelId: this.channelId
|
|
|
|
|
})
|
|
|
|
|
|
2020-06-04 03:00:36 +02:00
|
|
|
|
this.videoPublished = new Date(result.videoDetails.publishDate.replace('-', '/')).getTime()
|
2022-09-09 19:39:56 +02:00
|
|
|
|
try {
|
|
|
|
|
// workaround for description localization
|
2022-09-19 14:49:30 +02:00
|
|
|
|
const descriptionRuns = result.response.contents.twoColumnWatchNextResults.results.results.contents[1].videoSecondaryInfoRenderer.description?.runs
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(descriptionRuns)) {
|
|
|
|
|
// eslint-disable-next-line no-throw-literal
|
|
|
|
|
throw ['not an array', descriptionRuns]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fallbackDescription = result.player_response.videoDetails.shortDescription
|
|
|
|
|
|
|
|
|
|
// YouTube truncates links in the localised description
|
|
|
|
|
// so we need to fix them here, so that autolinker can do it's job properly later on
|
|
|
|
|
this.videoDescription = descriptionRuns
|
|
|
|
|
.map(run => this.processDescriptionPart(run, fallbackDescription))
|
|
|
|
|
.join('')
|
2022-09-09 19:39:56 +02:00
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to extract localised video description, falling back to the standard one.', err)
|
|
|
|
|
// if the workaround for localization fails, this sets the description to the potentially non-localized value
|
|
|
|
|
this.videoDescription = result.player_response.videoDetails.shortDescription
|
|
|
|
|
}
|
2020-09-22 03:30:48 +02:00
|
|
|
|
|
|
|
|
|
switch (this.thumbnailPreference) {
|
|
|
|
|
case 'start':
|
|
|
|
|
this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxres1.jpg`
|
|
|
|
|
break
|
|
|
|
|
case 'middle':
|
|
|
|
|
this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxres2.jpg`
|
|
|
|
|
break
|
|
|
|
|
case 'end':
|
|
|
|
|
this.thumbnail = `https://i.ytimg.com/vi/${this.videoId}/maxres3.jpg`
|
|
|
|
|
break
|
|
|
|
|
default:
|
2021-01-11 21:32:35 +01:00
|
|
|
|
this.thumbnail = result.videoDetails.thumbnails[result.videoDetails.thumbnails.length - 1].url
|
2020-09-22 03:30:48 +02:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-21 08:14:15 +02:00
|
|
|
|
this.isFamilyFriendly = result.videoDetails.isFamilySafe
|
2020-08-22 20:15:04 +02:00
|
|
|
|
this.recommendedVideos = result.related_videos.map((video) => {
|
|
|
|
|
video.videoId = video.id
|
2020-11-29 01:44:17 +01:00
|
|
|
|
video.authorId = video.author.id
|
2020-08-22 20:15:04 +02:00
|
|
|
|
video.viewCount = video.view_count
|
|
|
|
|
video.lengthSeconds = video.length_seconds
|
2020-11-29 01:44:17 +01:00
|
|
|
|
video.author = video.author.name
|
2021-03-06 19:20:33 +01:00
|
|
|
|
video.publishedText = video.published
|
2020-08-22 20:15:04 +02:00
|
|
|
|
return video
|
|
|
|
|
})
|
2020-10-06 04:27:32 +02:00
|
|
|
|
if (this.hideVideoLikesAndDislikes) {
|
|
|
|
|
this.videoLikeCount = null
|
|
|
|
|
this.videoDislikeCount = null
|
|
|
|
|
} else {
|
2020-12-31 12:22:26 +01:00
|
|
|
|
this.videoLikeCount = isNaN(result.videoDetails.likes) ? 0 : result.videoDetails.likes
|
|
|
|
|
this.videoDislikeCount = isNaN(result.videoDetails.dislikes) ? 0 : result.videoDetails.dislikes
|
2020-10-06 04:27:32 +02:00
|
|
|
|
}
|
2021-05-15 21:08:41 +02:00
|
|
|
|
this.isLive = result.player_response.videoDetails.isLive
|
2020-10-31 15:57:51 +01:00
|
|
|
|
this.isLiveContent = result.player_response.videoDetails.isLiveContent
|
2020-09-19 00:11:58 +02:00
|
|
|
|
this.isUpcoming = result.player_response.videoDetails.isUpcoming ? result.player_response.videoDetails.isUpcoming : false
|
2020-05-17 22:12:58 +02:00
|
|
|
|
|
2020-10-06 04:27:32 +02:00
|
|
|
|
if (this.videoDislikeCount === null && !this.hideVideoLikesAndDislikes) {
|
2020-06-27 19:59:20 +02:00
|
|
|
|
this.videoDislikeCount = 0
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-04 03:00:36 +02:00
|
|
|
|
const subCount = result.videoDetails.author.subscriber_count
|
2020-06-02 04:42:29 +02:00
|
|
|
|
|
2020-10-06 04:27:32 +02:00
|
|
|
|
if (typeof (subCount) !== 'undefined' && !this.hideChannelSubscriptions) {
|
2022-11-08 12:47:02 +01:00
|
|
|
|
if (subCount >= 10000) {
|
|
|
|
|
this.channelSubscriptionCountText = Intl.NumberFormat([this.currentLocale, 'en'], { notation: 'compact' }).format(subCount)
|
2020-06-27 19:59:20 +02:00
|
|
|
|
} else {
|
2022-11-08 12:47:02 +01:00
|
|
|
|
this.channelSubscriptionCountText = Intl.NumberFormat([this.currentLocale, 'en']).format(subCount)
|
2020-06-27 19:59:20 +02:00
|
|
|
|
}
|
2020-06-02 04:42:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-29 22:01:54 +02:00
|
|
|
|
const chapters = []
|
|
|
|
|
if (!this.hideChapters) {
|
2022-10-27 11:34:04 +02:00
|
|
|
|
const rawChapters = result.response.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(m => m.key === 'DESCRIPTION_CHAPTERS')?.value.chapters
|
2022-09-29 22:01:54 +02:00
|
|
|
|
if (rawChapters) {
|
|
|
|
|
for (const { chapterRenderer } of rawChapters) {
|
|
|
|
|
const start = chapterRenderer.timeRangeStartMillis / 1000
|
|
|
|
|
|
|
|
|
|
chapters.push({
|
|
|
|
|
title: chapterRenderer.title.simpleText,
|
2022-11-04 08:19:51 +01:00
|
|
|
|
timestamp: formatDurationAsTimestamp(start),
|
2022-09-29 22:01:54 +02:00
|
|
|
|
startSeconds: start,
|
|
|
|
|
endSeconds: 0,
|
|
|
|
|
thumbnail: chapterRenderer.thumbnail.thumbnails[0].url
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.addChaptersEndSeconds(chapters, result.videoDetails.lengthSeconds)
|
|
|
|
|
|
|
|
|
|
// prevent vue from adding reactivity which isn't needed
|
|
|
|
|
// as the chapter objects are read-only after this anyway
|
|
|
|
|
// the chapters are checked for every timeupdate event that the player emits
|
|
|
|
|
// this should lessen the performance and memory impact of the chapters
|
|
|
|
|
chapters.forEach(Object.freeze)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// only set this at the end so that there is only a single update to the view
|
|
|
|
|
this.videoChapters = chapters
|
|
|
|
|
|
2022-04-24 04:02:26 +02:00
|
|
|
|
if ((this.isLive && this.isLiveContent) && !this.isUpcoming) {
|
2020-08-30 16:21:01 +02:00
|
|
|
|
this.enableLegacyFormat()
|
2020-05-23 23:29:42 +02:00
|
|
|
|
|
|
|
|
|
this.videoSourceList = result.formats.filter((format) => {
|
|
|
|
|
if (typeof (format.mimeType) !== 'undefined') {
|
|
|
|
|
return format.mimeType.includes('video/ts')
|
|
|
|
|
}
|
2020-06-05 04:44:58 +02:00
|
|
|
|
|
|
|
|
|
return format.itag === 300 || format.itag === 301
|
2020-05-23 23:29:42 +02:00
|
|
|
|
}).map((format) => {
|
2020-06-05 04:44:58 +02:00
|
|
|
|
let qualityLabel
|
|
|
|
|
|
|
|
|
|
if (format.itag === 300) {
|
|
|
|
|
qualityLabel = '720p'
|
|
|
|
|
} else if (format.itag === 301) {
|
|
|
|
|
qualityLabel = '1080p'
|
|
|
|
|
} else {
|
|
|
|
|
qualityLabel = format.qualityLabel
|
|
|
|
|
}
|
2020-05-23 23:29:42 +02:00
|
|
|
|
return {
|
|
|
|
|
url: format.url,
|
|
|
|
|
type: 'application/x-mpegURL',
|
2020-05-17 22:12:58 +02:00
|
|
|
|
label: 'Dash',
|
2020-06-05 04:44:58 +02:00
|
|
|
|
qualityLabel: qualityLabel
|
2020-05-23 23:29:42 +02:00
|
|
|
|
}
|
2020-06-05 04:44:58 +02:00
|
|
|
|
}).sort((a, b) => {
|
|
|
|
|
const qualityA = parseInt(a.qualityLabel.replace('p', ''))
|
|
|
|
|
const qualityB = parseInt(b.qualityLabel.replace('p', ''))
|
|
|
|
|
return qualityA - qualityB
|
2020-09-25 04:35:13 +02:00
|
|
|
|
}).reverse()
|
2020-06-04 03:00:36 +02:00
|
|
|
|
|
2020-08-07 22:06:48 +02:00
|
|
|
|
if (this.videoSourceList.length === 0) {
|
|
|
|
|
this.activeSourceList = result.player_response.streamingData.formats
|
|
|
|
|
} else {
|
|
|
|
|
this.activeSourceList = this.videoSourceList
|
|
|
|
|
}
|
2020-09-16 04:07:54 +02:00
|
|
|
|
} else if (this.isUpcoming) {
|
2020-09-25 16:14:49 +02:00
|
|
|
|
const startTimestamp = result.videoDetails.liveBroadcastDetails.startTimestamp
|
|
|
|
|
|
|
|
|
|
if (typeof startTimestamp !== 'undefined') {
|
|
|
|
|
const upcomingTimestamp = new Date(result.videoDetails.liveBroadcastDetails.startTimestamp)
|
2022-09-22 02:49:59 +02:00
|
|
|
|
const timestampOptions = {
|
|
|
|
|
month: 'long',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
hour: 'numeric',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
}
|
|
|
|
|
if (new Date().getFullYear() < upcomingTimestamp.getFullYear()) {
|
|
|
|
|
Object.defineProperty(timestampOptions, 'year', {
|
|
|
|
|
value: 'numeric'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
this.upcomingTimestamp = Intl.DateTimeFormat(this.currentLocale, timestampOptions).format(upcomingTimestamp)
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
|
|
|
|
|
let upcomingTimeLeft = upcomingTimestamp - new Date()
|
|
|
|
|
|
|
|
|
|
// Convert from ms to second to minute
|
|
|
|
|
upcomingTimeLeft = (upcomingTimeLeft / 1000) / 60
|
2022-09-25 01:23:45 +02:00
|
|
|
|
let timeUnit = 'minute'
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
|
|
|
|
|
// Youtube switches to showing time left in minutes at 120 minutes remaining
|
|
|
|
|
if (upcomingTimeLeft > 120) {
|
|
|
|
|
upcomingTimeLeft = upcomingTimeLeft / 60
|
2022-09-25 01:23:45 +02:00
|
|
|
|
timeUnit = 'hour'
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-25 01:23:45 +02:00
|
|
|
|
if (timeUnit === 'hour' && upcomingTimeLeft > 24) {
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
upcomingTimeLeft = upcomingTimeLeft / 24
|
2022-09-25 01:23:45 +02:00
|
|
|
|
timeUnit = 'day'
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Value after decimal not to be displayed
|
|
|
|
|
// e.g. > 2 days = display as `2 days`
|
|
|
|
|
upcomingTimeLeft = Math.floor(upcomingTimeLeft)
|
|
|
|
|
|
|
|
|
|
// Displays when less than a minute remains
|
|
|
|
|
// Looks better than `Premieres in x seconds`
|
|
|
|
|
if (upcomingTimeLeft < 1) {
|
2022-09-25 01:23:45 +02:00
|
|
|
|
this.upcomingTimeLeft = this.$t('Video.Published.In less than a minute').toLowerCase()
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
} else {
|
|
|
|
|
// TODO a I18n entry for time format might be needed here
|
2022-09-25 01:23:45 +02:00
|
|
|
|
this.upcomingTimeLeft = new Intl.RelativeTimeFormat(this.currentLocale).format(upcomingTimeLeft, timeUnit)
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
}
|
2020-09-25 16:14:49 +02:00
|
|
|
|
} else {
|
|
|
|
|
this.upcomingTimestamp = null
|
Display time remaining until video goes live (#2501)
* display time left until video premiers:
* video premiere display time left with time units
Displays time left in seconds, minutes, hours, and days.
This depends on how much time is left.
* premiere time left, display time in singular if needed
also simplified the big if block
* premiere time left, display time unit in lowercase
* Add Starting Soon string to locale file
* apply fixes reported by linter
* premiere time left, add suggested changes
Better temp variable scoping, flatten nested code, rename temp variables, use string intepolation
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
* replace tabs with spaces
tabs where used in some places in the suggested code
* display time left, remove "starting soon" string
Since upcomingTimeStamp will be null when the time has passed the scheduled timestamp
it doesn't make sense to use something that will rarely be displayed.
e.g. a user has to click on the video with less than a second remaing until it goes live for it to be displayed
it would also be displayed as "Premieres in Starting soon" which doesn't make sense
* display 'less than a minute' instead of exactly how many seconds remain
Looks better and works for values less than 0
Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
2022-09-05 23:45:45 +02:00
|
|
|
|
this.upcomingTimeLeft = null
|
2020-09-25 16:14:49 +02:00
|
|
|
|
}
|
2020-05-17 22:12:58 +02:00
|
|
|
|
} else {
|
2020-08-20 04:39:44 +02:00
|
|
|
|
this.videoLengthSeconds = parseInt(result.videoDetails.lengthSeconds)
|
2020-09-30 22:37:23 +02:00
|
|
|
|
if (result.player_response.streamingData !== undefined) {
|
2021-04-30 23:43:12 +02:00
|
|
|
|
if (typeof (result.player_response.streamingData.formats) !== 'undefined') {
|
|
|
|
|
this.videoSourceList = result.player_response.streamingData.formats.reverse()
|
|
|
|
|
} else {
|
|
|
|
|
this.videoSourceList = result.player_response.streamingData.adaptiveFormats.reverse()
|
|
|
|
|
}
|
2021-05-15 21:08:41 +02:00
|
|
|
|
this.adaptiveFormats = this.videoSourceList
|
2021-04-30 23:43:12 +02:00
|
|
|
|
this.downloadLinks = result.formats.filter((format) => {
|
|
|
|
|
return typeof format.mimeType !== 'undefined'
|
|
|
|
|
}).map((format) => {
|
2020-10-08 21:01:46 +02:00
|
|
|
|
const qualityLabel = format.qualityLabel || format.bitrate
|
|
|
|
|
const itag = format.itag
|
|
|
|
|
const fps = format.fps ? (format.fps + 'fps') : 'kbps'
|
|
|
|
|
const type = format.mimeType.match(/.*;/)[0].replace(';', '')
|
|
|
|
|
let label = `${qualityLabel} ${fps} - ${type}`
|
|
|
|
|
|
|
|
|
|
if (itag !== 18 && itag !== 22) {
|
|
|
|
|
if (type.includes('video')) {
|
|
|
|
|
label += ` ${this.$t('Video.video only')}`
|
|
|
|
|
} else {
|
|
|
|
|
label += ` ${this.$t('Video.audio only')}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const object = {
|
|
|
|
|
url: format.url,
|
|
|
|
|
label: label
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return object
|
|
|
|
|
})
|
2021-03-17 02:28:25 +01:00
|
|
|
|
|
|
|
|
|
const captionTracks =
|
|
|
|
|
result.player_response.captions &&
|
|
|
|
|
result.player_response.captions.playerCaptionsTracklistRenderer
|
|
|
|
|
.captionTracks
|
|
|
|
|
|
|
|
|
|
if (typeof captionTracks !== 'undefined') {
|
2021-05-11 19:25:47 +02:00
|
|
|
|
const locale = this.$i18n.locale
|
2021-04-12 23:00:07 +02:00
|
|
|
|
if (locale !== null) {
|
|
|
|
|
const standardLocale = locale.replace('_', '-')
|
|
|
|
|
const noLocaleCaption = !captionTracks.some(track =>
|
|
|
|
|
track.languageCode === standardLocale && track.kind !== 'asr'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (!standardLocale.startsWith('en') && noLocaleCaption) {
|
2022-05-27 04:46:49 +02:00
|
|
|
|
captionTracks.forEach((caption) => {
|
|
|
|
|
this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, caption.baseUrl)
|
|
|
|
|
})
|
2021-04-12 23:00:07 +02:00
|
|
|
|
}
|
2021-03-19 03:36:45 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-17 02:28:25 +01:00
|
|
|
|
this.captionHybridList = this.createCaptionPromiseList(captionTracks)
|
|
|
|
|
|
|
|
|
|
const captionLinks = captionTracks.map((caption) => {
|
2020-10-19 17:51:37 +02:00
|
|
|
|
const label = `${caption.name.simpleText} (${caption.languageCode}) - text/vtt`
|
2021-03-17 02:28:25 +01:00
|
|
|
|
|
|
|
|
|
return {
|
2020-10-19 17:51:37 +02:00
|
|
|
|
url: caption.baseUrl,
|
|
|
|
|
label: label
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.downloadLinks = this.downloadLinks.concat(captionLinks)
|
|
|
|
|
}
|
2020-09-30 22:37:23 +02:00
|
|
|
|
} else {
|
|
|
|
|
// video might be region locked or something else. This leads to no formats being available
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(
|
|
|
|
|
this.$t('This video is unavailable because of missing formats. This can happen due to country unavailability.'),
|
|
|
|
|
7000
|
|
|
|
|
)
|
2020-09-30 23:01:36 +02:00
|
|
|
|
this.handleVideoEnded()
|
2020-09-30 22:37:23 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-06-01 04:47:22 +02:00
|
|
|
|
|
2020-09-16 04:07:54 +02:00
|
|
|
|
if (typeof result.player_response.streamingData.adaptiveFormats !== 'undefined') {
|
2021-10-12 23:03:22 +02:00
|
|
|
|
const adaptiveFormats = result.player_response.streamingData.adaptiveFormats
|
|
|
|
|
this.adaptiveFormats = adaptiveFormats
|
2020-10-04 23:01:59 +02:00
|
|
|
|
if (this.proxyVideos) {
|
|
|
|
|
this.dashSrc = await this.createInvidiousDashManifest()
|
|
|
|
|
} else {
|
2021-04-30 23:18:45 +02:00
|
|
|
|
this.dashSrc = await this.createLocalDashManifest(adaptiveFormats)
|
2020-10-04 23:01:59 +02:00
|
|
|
|
}
|
2020-09-16 04:07:54 +02:00
|
|
|
|
|
|
|
|
|
this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => {
|
|
|
|
|
return format.mimeType.includes('audio')
|
2020-10-05 03:20:30 +02:00
|
|
|
|
}).sort((a, b) => {
|
|
|
|
|
return a.bitrate - b.bitrate
|
|
|
|
|
}).map((format, index) => {
|
|
|
|
|
const label = (x) => {
|
|
|
|
|
switch (x) {
|
|
|
|
|
case 0:
|
|
|
|
|
return this.$t('Video.Audio.Low')
|
|
|
|
|
case 1:
|
|
|
|
|
return this.$t('Video.Audio.Medium')
|
|
|
|
|
case 2:
|
|
|
|
|
return this.$t('Video.Audio.High')
|
|
|
|
|
case 3:
|
|
|
|
|
return this.$t('Video.Audio.Best')
|
|
|
|
|
default:
|
|
|
|
|
return format.bitrate
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-16 04:07:54 +02:00
|
|
|
|
return {
|
|
|
|
|
url: format.url,
|
|
|
|
|
type: format.mimeType,
|
|
|
|
|
label: 'Audio',
|
2020-10-05 03:20:30 +02:00
|
|
|
|
qualityLabel: label(index)
|
2020-09-16 04:07:54 +02:00
|
|
|
|
}
|
2020-10-05 03:20:30 +02:00
|
|
|
|
}).reverse()
|
2020-09-16 04:07:54 +02:00
|
|
|
|
|
|
|
|
|
if (this.activeFormat === 'audio') {
|
|
|
|
|
this.activeSourceList = this.audioSourceList
|
|
|
|
|
} else {
|
|
|
|
|
this.activeSourceList = this.videoSourceList
|
2020-06-01 04:47:22 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.activeSourceList = this.videoSourceList
|
2020-09-16 04:07:54 +02:00
|
|
|
|
this.audioSourceList = null
|
|
|
|
|
this.dashSrc = null
|
|
|
|
|
this.enableLegacyFormat()
|
2020-06-01 04:47:22 +02:00
|
|
|
|
}
|
2020-02-28 04:29:30 +01:00
|
|
|
|
|
2020-09-10 00:58:35 +02:00
|
|
|
|
if (typeof result.player_response.storyboards !== 'undefined') {
|
|
|
|
|
const templateUrl = result.player_response.storyboards.playerStoryboardSpecRenderer.spec
|
2022-12-29 02:19:48 +01:00
|
|
|
|
await this.createLocalStoryboardUrls(templateUrl)
|
2020-09-10 00:58:35 +02:00
|
|
|
|
}
|
2020-08-19 01:32:44 +02:00
|
|
|
|
}
|
2020-08-15 13:03:30 +02:00
|
|
|
|
|
2020-09-27 00:05:44 +02:00
|
|
|
|
this.isLoading = false
|
2020-10-31 15:36:35 +01:00
|
|
|
|
this.updateTitle()
|
2020-02-28 04:29:30 +01:00
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
2020-08-09 05:15:00 +02:00
|
|
|
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
2022-10-17 14:33:20 +02:00
|
|
|
|
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
2022-10-18 10:15:28 +02:00
|
|
|
|
copyToClipboard(err)
|
2020-08-05 05:44:34 +02:00
|
|
|
|
})
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.error(err)
|
2022-09-17 10:19:31 +02:00
|
|
|
|
if (this.backendPreference === 'local' && this.backendFallback && !err.toString().includes('private')) {
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Falling back to Invidious API'))
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.getVideoInformationInvidious()
|
|
|
|
|
} else {
|
|
|
|
|
this.isLoading = false
|
|
|
|
|
}
|
|
|
|
|
})
|
2020-02-16 19:30:00 +01:00
|
|
|
|
},
|
|
|
|
|
|
2022-06-21 04:33:42 +02:00
|
|
|
|
getVideoInformationInvidious: function () {
|
2020-02-16 19:30:00 +01:00
|
|
|
|
if (this.firstLoad) {
|
|
|
|
|
this.isLoading = true
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-30 16:21:01 +02:00
|
|
|
|
this.dashSrc = this.createInvidiousDashManifest()
|
2021-07-03 03:55:56 +02:00
|
|
|
|
this.videoStoryboardSrc = `${this.currentInvidiousInstance}/api/v1/storyboards/${this.videoId}?height=90`
|
2020-08-30 16:21:01 +02:00
|
|
|
|
|
2021-05-22 01:56:32 +02:00
|
|
|
|
this.invidiousGetVideoInformation(this.videoId)
|
2020-02-28 04:29:30 +01:00
|
|
|
|
.then(result => {
|
2020-12-02 19:36:08 +01:00
|
|
|
|
if (result.error) {
|
|
|
|
|
throw new Error(result.error)
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.videoTitle = result.title
|
|
|
|
|
this.videoViewCount = result.viewCount
|
2020-10-06 04:27:32 +02:00
|
|
|
|
if (this.hideVideoLikesAndDislikes) {
|
|
|
|
|
this.videoLikeCount = null
|
|
|
|
|
this.videoDislikeCount = null
|
|
|
|
|
} else {
|
|
|
|
|
this.videoLikeCount = result.likeCount
|
|
|
|
|
this.videoDislikeCount = result.dislikeCount
|
|
|
|
|
}
|
|
|
|
|
if (this.hideChannelSubscriptions) {
|
|
|
|
|
this.channelSubscriptionCountText = ''
|
|
|
|
|
} else {
|
|
|
|
|
this.channelSubscriptionCountText = result.subCountText || 'FT-0'
|
|
|
|
|
}
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.channelId = result.authorId
|
|
|
|
|
this.channelName = result.author
|
2022-06-03 14:52:35 +02:00
|
|
|
|
const channelThumb = result.authorThumbnails[1]
|
|
|
|
|
this.channelThumbnail = channelThumb ? channelThumb.url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`) : ''
|
|
|
|
|
this.updateSubscriptionDetails({
|
|
|
|
|
channelThumbnailUrl: channelThumb?.url,
|
|
|
|
|
channelName: result.author,
|
|
|
|
|
channelId: result.authorId
|
|
|
|
|
})
|
|
|
|
|
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.videoPublished = result.published * 1000
|
|
|
|
|
this.videoDescriptionHtml = result.descriptionHtml
|
|
|
|
|
this.recommendedVideos = result.recommendedVideos
|
2021-10-12 23:03:22 +02:00
|
|
|
|
this.adaptiveFormats = result.adaptiveFormats.map((format) => {
|
|
|
|
|
format.bitrate = parseInt(format.bitrate)
|
2021-10-14 05:11:51 +02:00
|
|
|
|
if (typeof format.resolution !== 'undefined') {
|
|
|
|
|
format.height = parseInt(format.resolution.replace('p', ''))
|
|
|
|
|
}
|
2021-10-12 23:03:22 +02:00
|
|
|
|
return format
|
|
|
|
|
})
|
2020-05-23 23:29:42 +02:00
|
|
|
|
this.isLive = result.liveNow
|
2022-06-21 08:14:15 +02:00
|
|
|
|
this.isFamilyFriendly = result.isFamilyFriendly
|
2021-03-17 02:28:25 +01:00
|
|
|
|
this.captionHybridList = result.captions.map(caption => {
|
2021-07-03 03:55:56 +02:00
|
|
|
|
caption.url = this.currentInvidiousInstance + caption.url
|
2020-02-28 04:29:30 +01:00
|
|
|
|
caption.type = ''
|
|
|
|
|
caption.dataSource = 'invidious'
|
|
|
|
|
return caption
|
|
|
|
|
})
|
2020-02-16 19:30:00 +01:00
|
|
|
|
|
2020-09-22 03:30:48 +02:00
|
|
|
|
switch (this.thumbnailPreference) {
|
|
|
|
|
case 'start':
|
2021-07-03 03:55:56 +02:00
|
|
|
|
this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres1.jpg`
|
2020-09-22 03:30:48 +02:00
|
|
|
|
break
|
|
|
|
|
case 'middle':
|
2021-07-03 03:55:56 +02:00
|
|
|
|
this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres2.jpg`
|
2020-09-22 03:30:48 +02:00
|
|
|
|
break
|
|
|
|
|
case 'end':
|
2021-07-03 03:55:56 +02:00
|
|
|
|
this.thumbnail = `${this.currentInvidiousInstance}/vi/${this.videoId}/maxres3.jpg`
|
2020-09-22 03:30:48 +02:00
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
this.thumbnail = result.videoThumbnails[0].url
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-29 22:01:54 +02:00
|
|
|
|
const chapters = []
|
|
|
|
|
if (!this.hideChapters) {
|
|
|
|
|
// HH:MM:SS Text
|
|
|
|
|
// MM:SS Text
|
|
|
|
|
// HH:MM:SS - Text // separator is one of '-', '–', '•', '—'
|
|
|
|
|
// MM:SS - Text
|
|
|
|
|
// HH:MM:SS - HH:MM:SS - Text // end timestamp is ignored, separator is one of '-', '–', '—'
|
|
|
|
|
// HH:MM - HH:MM - Text // end timestamp is ignored
|
|
|
|
|
const chapterMatches = result.description.matchAll(/^(?<timestamp>((?<hours>[0-9]+):)?(?<minutes>[0-9]+):(?<seconds>[0-9]+))(\s*[-–—]\s*(?:[0-9]+:)?[0-9]+:[0-9]+)?\s+([-–•—]\s*)?(?<title>.+)$/gm)
|
|
|
|
|
|
|
|
|
|
for (const { groups } of chapterMatches) {
|
|
|
|
|
let start = 60 * Number(groups.minutes) + Number(groups.seconds)
|
|
|
|
|
|
|
|
|
|
if (groups.hours) {
|
|
|
|
|
start += 3600 * Number(groups.hours)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// replace previous chapter with current one if they have an identical start time
|
|
|
|
|
if (chapters.length > 0 && chapters[chapters.length - 1].startSeconds === start) {
|
|
|
|
|
chapters.pop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chapters.push({
|
|
|
|
|
title: groups.title.trim(),
|
|
|
|
|
timestamp: groups.timestamp,
|
|
|
|
|
startSeconds: start,
|
|
|
|
|
endSeconds: 0
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chapters.length > 0) {
|
|
|
|
|
this.addChaptersEndSeconds(chapters, result.lengthSeconds)
|
|
|
|
|
|
|
|
|
|
// prevent vue from adding reactivity which isn't needed
|
|
|
|
|
// as the chapter objects are read-only after this anyway
|
|
|
|
|
// the chapters are checked for every timeupdate event that the player emits
|
|
|
|
|
// this should lessen the performance and memory impact of the chapters
|
|
|
|
|
chapters.forEach(Object.freeze)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.videoChapters = chapters
|
|
|
|
|
|
2020-05-23 23:29:42 +02:00
|
|
|
|
if (this.isLive) {
|
|
|
|
|
this.showLegacyPlayer = true
|
|
|
|
|
this.showDashPlayer = false
|
|
|
|
|
this.activeFormat = 'legacy'
|
|
|
|
|
|
|
|
|
|
this.videoSourceList = [
|
|
|
|
|
{
|
|
|
|
|
url: result.hlsUrl,
|
|
|
|
|
type: 'application/x-mpegURL',
|
|
|
|
|
label: 'Dash',
|
|
|
|
|
qualityLabel: 'Live'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// Grabs the adaptive formats from Invidious. Might be worth making these work.
|
|
|
|
|
// The type likely needs to be changed in order for these to be played properly.
|
|
|
|
|
// this.videoSourceList = result.adaptiveFormats.filter((format) => {
|
|
|
|
|
// if (typeof (format.type) !== 'undefined') {
|
|
|
|
|
// return format.type.includes('video/mp4')
|
|
|
|
|
// }
|
|
|
|
|
// }).map((format) => {
|
|
|
|
|
// return {
|
|
|
|
|
// url: format.url,
|
|
|
|
|
// type: 'application/x-mpegURL',
|
|
|
|
|
// label: 'Dash',
|
|
|
|
|
// qualityLabel: format.qualityLabel
|
|
|
|
|
// }
|
|
|
|
|
// })
|
2020-06-04 03:00:36 +02:00
|
|
|
|
|
|
|
|
|
this.activeSourceList = this.videoSourceList
|
2020-05-23 23:29:42 +02:00
|
|
|
|
} else if (this.forceLocalBackendForLegacy) {
|
2020-03-01 04:37:02 +01:00
|
|
|
|
this.getLegacyFormats()
|
2020-04-14 04:59:25 +02:00
|
|
|
|
} else {
|
2020-08-20 04:39:44 +02:00
|
|
|
|
this.videoLengthSeconds = result.lengthSeconds
|
2020-04-14 04:59:25 +02:00
|
|
|
|
this.videoSourceList = result.formatStreams.reverse()
|
2020-06-01 04:47:22 +02:00
|
|
|
|
|
2020-10-08 21:01:46 +02:00
|
|
|
|
this.downloadLinks = result.adaptiveFormats.concat(this.videoSourceList).map((format) => {
|
|
|
|
|
const qualityLabel = format.qualityLabel || format.bitrate
|
|
|
|
|
const itag = parseInt(format.itag)
|
|
|
|
|
const fps = format.fps ? (format.fps + 'fps') : 'kbps'
|
|
|
|
|
const type = format.type.match(/.*;/)[0].replace(';', '')
|
|
|
|
|
let label = `${qualityLabel} ${fps} - ${type}`
|
|
|
|
|
|
|
|
|
|
if (itag !== 18 && itag !== 22) {
|
|
|
|
|
if (type.includes('video')) {
|
|
|
|
|
label += ` ${this.$t('Video.video only')}`
|
|
|
|
|
} else {
|
|
|
|
|
label += ` ${this.$t('Video.audio only')}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const object = {
|
|
|
|
|
url: format.url,
|
|
|
|
|
label: label
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return object
|
2020-10-19 21:16:56 +02:00
|
|
|
|
}).reverse().concat(result.captions.map((caption) => {
|
|
|
|
|
const label = `${caption.label} (${caption.languageCode}) - text/vtt`
|
|
|
|
|
const object = {
|
|
|
|
|
url: caption.url,
|
|
|
|
|
label: label
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return object
|
|
|
|
|
}))
|
2020-10-08 21:01:46 +02:00
|
|
|
|
|
2020-06-01 04:47:22 +02:00
|
|
|
|
this.audioSourceList = result.adaptiveFormats.filter((format) => {
|
|
|
|
|
return format.type.includes('audio')
|
|
|
|
|
}).map((format) => {
|
|
|
|
|
return {
|
|
|
|
|
url: format.url,
|
|
|
|
|
type: format.type,
|
|
|
|
|
label: 'Audio',
|
|
|
|
|
qualityLabel: parseInt(format.bitrate)
|
|
|
|
|
}
|
|
|
|
|
}).sort((a, b) => {
|
|
|
|
|
return a.qualityLabel - b.qualityLabel
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (this.activeFormat === 'audio') {
|
|
|
|
|
this.activeSourceList = this.audioSourceList
|
|
|
|
|
} else {
|
|
|
|
|
this.activeSourceList = this.videoSourceList
|
|
|
|
|
}
|
2020-03-01 04:37:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-31 15:36:35 +01:00
|
|
|
|
this.updateTitle()
|
2020-10-31 16:23:26 +01:00
|
|
|
|
|
|
|
|
|
this.isLoading = false
|
2020-02-28 04:29:30 +01:00
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.error(err)
|
2020-08-09 05:15:00 +02:00
|
|
|
|
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
2022-10-17 14:33:20 +02:00
|
|
|
|
showToast(`${errorMessage}: ${err.responseText}`, 10000, () => {
|
2022-10-18 10:15:28 +02:00
|
|
|
|
copyToClipboard(err.responseText)
|
2020-08-05 05:44:34 +02:00
|
|
|
|
})
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.error(err)
|
2020-02-28 04:29:30 +01:00
|
|
|
|
if (this.backendPreference === 'invidious' && this.backendFallback) {
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Falling back to Local API'))
|
2020-02-28 04:29:30 +01:00
|
|
|
|
this.getVideoInformationLocal()
|
|
|
|
|
} else {
|
|
|
|
|
this.isLoading = false
|
|
|
|
|
}
|
|
|
|
|
})
|
2020-02-19 04:31:10 +01:00
|
|
|
|
},
|
|
|
|
|
|
2022-09-19 14:49:30 +02:00
|
|
|
|
processDescriptionPart(part, fallbackDescription) {
|
|
|
|
|
const timestampRegex = /^([0-9]+:)?[0-9]+:[0-9]+$/
|
|
|
|
|
|
|
|
|
|
if (typeof part.navigationEndpoint === 'undefined' || part.navigationEndpoint === null || part.text.startsWith('#')) {
|
|
|
|
|
return part.text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (part.navigationEndpoint.urlEndpoint) {
|
|
|
|
|
const urlWithTracking = part.navigationEndpoint.urlEndpoint.url
|
|
|
|
|
const url = new URL(urlWithTracking)
|
|
|
|
|
|
|
|
|
|
if (url.hostname === 'www.youtube.com' && url.pathname === '/redirect' && url.searchParams.has('q')) {
|
|
|
|
|
// remove utm tracking parameters
|
|
|
|
|
const realURL = new URL(url.searchParams.get('q'))
|
|
|
|
|
|
|
|
|
|
realURL.searchParams.delete('utm_source')
|
|
|
|
|
realURL.searchParams.delete('utm_medium')
|
|
|
|
|
realURL.searchParams.delete('utm_campaign')
|
|
|
|
|
realURL.searchParams.delete('utm_term')
|
|
|
|
|
realURL.searchParams.delete('utm_content')
|
|
|
|
|
|
|
|
|
|
return realURL.toString()
|
|
|
|
|
} else if (fallbackDescription.includes(urlWithTracking)) {
|
|
|
|
|
// this is probably a special YouTube URL like http://www.youtube.com/approachingnirvana
|
|
|
|
|
// only use it if it exists in the fallback description
|
|
|
|
|
// otherwise assume YouTube has changed it's tracking URLs and throw an error
|
|
|
|
|
return urlWithTracking
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-throw-literal
|
|
|
|
|
throw `Failed to extract real URL from tracking URL: ${urlWithTracking}`
|
|
|
|
|
} else if (part.navigationEndpoint.watchEndpoint) {
|
|
|
|
|
if (timestampRegex.test(part.text)) {
|
|
|
|
|
return part.text
|
|
|
|
|
}
|
|
|
|
|
const watchEndpoint = part.navigationEndpoint.watchEndpoint
|
|
|
|
|
|
|
|
|
|
let videoURL = `https://www.youtube.com/watch?v=${watchEndpoint.videoId}`
|
|
|
|
|
if (watchEndpoint.startTimeSeconds !== 0) {
|
|
|
|
|
videoURL += `&t=${watchEndpoint.startTimeSeconds}s`
|
|
|
|
|
}
|
|
|
|
|
return videoURL
|
|
|
|
|
} else {
|
|
|
|
|
// Some YouTube URLs don't have the urlEndpoint so we handle them here
|
|
|
|
|
|
2022-12-31 11:07:53 +01:00
|
|
|
|
const { browseEndpoint, commandMetadata: { webCommandMetadata } } = part.navigationEndpoint
|
|
|
|
|
// channel handle
|
|
|
|
|
if (webCommandMetadata.webPageType === 'WEB_PAGE_TYPE_CHANNEL' && part.text.startsWith('@')) {
|
|
|
|
|
return `<a href="https://www.youtube.com/channel/${browseEndpoint.browseId}">${part.text}</a>`
|
|
|
|
|
} else {
|
|
|
|
|
const path = webCommandMetadata.url
|
|
|
|
|
return `https://www.youtube.com${path}`
|
|
|
|
|
}
|
2022-09-19 14:49:30 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2022-09-29 22:01:54 +02:00
|
|
|
|
addChaptersEndSeconds: function (chapters, videoLengthSeconds) {
|
|
|
|
|
for (let i = 0; i < chapters.length - 1; i++) {
|
|
|
|
|
chapters[i].endSeconds = chapters[i + 1].startSeconds
|
|
|
|
|
}
|
|
|
|
|
chapters.at(-1).endSeconds = videoLengthSeconds
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateCurrentChapter: function () {
|
|
|
|
|
const chapters = this.videoChapters
|
|
|
|
|
const currentSeconds = this.getTimestamp()
|
|
|
|
|
const currentChapterStart = chapters[this.videoCurrentChapterIndex].startSeconds
|
|
|
|
|
|
|
|
|
|
if (currentSeconds !== currentChapterStart) {
|
|
|
|
|
let i = currentSeconds < currentChapterStart ? 0 : this.videoCurrentChapterIndex
|
|
|
|
|
|
|
|
|
|
for (; i < chapters.length; i++) {
|
|
|
|
|
if (currentSeconds < chapters[i].endSeconds) {
|
|
|
|
|
this.videoCurrentChapterIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-08-20 04:39:44 +02:00
|
|
|
|
addToHistory: function (watchProgress) {
|
|
|
|
|
const videoData = {
|
|
|
|
|
videoId: this.videoId,
|
|
|
|
|
title: this.videoTitle,
|
|
|
|
|
author: this.channelName,
|
|
|
|
|
authorId: this.channelId,
|
|
|
|
|
published: this.videoPublished,
|
|
|
|
|
description: this.videoDescription,
|
|
|
|
|
viewCount: this.videoViewCount,
|
|
|
|
|
lengthSeconds: this.videoLengthSeconds,
|
|
|
|
|
watchProgress: watchProgress,
|
|
|
|
|
timeWatched: new Date().getTime(),
|
|
|
|
|
isLive: false,
|
|
|
|
|
paid: false,
|
|
|
|
|
type: 'video'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.updateHistory(videoData)
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-21 20:38:46 +02:00
|
|
|
|
handleWatchProgress: function () {
|
|
|
|
|
if (this.rememberHistory && !this.isUpcoming && !this.isLoading && !this.isLive) {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
|
|
|
|
|
if (player !== null && this.saveWatchedProgress) {
|
|
|
|
|
const currentTime = this.getWatchedProgress()
|
|
|
|
|
const payload = {
|
|
|
|
|
videoId: this.videoId,
|
|
|
|
|
watchProgress: currentTime
|
|
|
|
|
}
|
|
|
|
|
this.updateWatchProgress(payload)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-08-20 04:39:44 +02:00
|
|
|
|
checkIfWatched: function () {
|
|
|
|
|
const historyIndex = this.historyCache.findIndex((video) => {
|
|
|
|
|
return video.videoId === this.videoId
|
|
|
|
|
})
|
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
|
if (!this.isLive) {
|
|
|
|
|
if (this.timestamp) {
|
|
|
|
|
if (this.timestamp < 0) {
|
|
|
|
|
this.$refs.videoPlayer.player.currentTime(0)
|
|
|
|
|
} else if (this.timestamp > (this.videoLengthSeconds - 10)) {
|
|
|
|
|
this.$refs.videoPlayer.player.currentTime(this.videoLengthSeconds - 10)
|
|
|
|
|
} else {
|
|
|
|
|
this.$refs.videoPlayer.player.currentTime(this.timestamp)
|
|
|
|
|
}
|
|
|
|
|
} else if (historyIndex !== -1) {
|
|
|
|
|
const watchProgress = this.historyCache[historyIndex].watchProgress
|
2020-09-25 00:33:21 +02:00
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
|
if (watchProgress < (this.videoLengthSeconds - 10)) {
|
|
|
|
|
this.$refs.videoPlayer.player.currentTime(watchProgress)
|
|
|
|
|
}
|
2020-09-25 00:33:21 +02:00
|
|
|
|
}
|
2020-08-20 04:39:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
|
if (this.rememberHistory) {
|
|
|
|
|
if (this.timestamp) {
|
|
|
|
|
this.addToHistory(this.timestamp)
|
|
|
|
|
} else if (historyIndex !== -1) {
|
|
|
|
|
this.addToHistory(this.historyCache[historyIndex].watchProgress)
|
|
|
|
|
} else {
|
|
|
|
|
this.addToHistory(0)
|
|
|
|
|
}
|
2020-08-20 04:39:44 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
|
checkIfPlaylist: function () {
|
|
|
|
|
if (typeof (this.$route.query) !== 'undefined') {
|
|
|
|
|
this.playlistId = this.$route.query.playlistId
|
|
|
|
|
|
|
|
|
|
if (typeof (this.playlistId) !== 'undefined') {
|
|
|
|
|
this.watchingPlaylist = true
|
|
|
|
|
} else {
|
|
|
|
|
this.watchingPlaylist = false
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.watchingPlaylist = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-03-06 20:03:40 +01:00
|
|
|
|
checkIfTimestamp: function () {
|
|
|
|
|
if (typeof (this.$route.query) !== 'undefined') {
|
|
|
|
|
try {
|
|
|
|
|
this.timestamp = parseInt(this.$route.query.timestamp)
|
|
|
|
|
} catch {
|
|
|
|
|
this.timestamp = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-03-01 04:37:02 +01:00
|
|
|
|
getLegacyFormats: function () {
|
2021-05-22 01:56:32 +02:00
|
|
|
|
this.ytGetVideoInformation(this.videoId)
|
2020-03-01 04:37:02 +01:00
|
|
|
|
.then(result => {
|
|
|
|
|
this.videoSourceList = result.player_response.streamingData.formats
|
|
|
|
|
})
|
2020-08-05 05:44:34 +02:00
|
|
|
|
.catch(err => {
|
2020-08-09 05:15:00 +02:00
|
|
|
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
2022-10-17 14:33:20 +02:00
|
|
|
|
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
2022-10-18 10:15:28 +02:00
|
|
|
|
copyToClipboard(err)
|
2020-08-05 05:44:34 +02:00
|
|
|
|
})
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.error(err)
|
2022-09-15 10:59:09 +02:00
|
|
|
|
if (!process.env.IS_ELECTRON || (this.backendPreference === 'local' && this.backendFallback)) {
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Falling back to Invidious API'))
|
2020-08-05 05:44:34 +02:00
|
|
|
|
this.getVideoInformationInvidious()
|
|
|
|
|
}
|
|
|
|
|
})
|
2020-03-01 04:37:02 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
enableDashFormat: function () {
|
2020-05-23 23:29:42 +02:00
|
|
|
|
if (this.activeFormat === 'dash' || this.isLive) {
|
2020-02-20 21:58:21 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-16 04:07:54 +02:00
|
|
|
|
if (this.dashSrc === null) {
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Change Format.Dash formats are not available for this video'))
|
2020-09-16 04:07:54 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-09-23 00:27:53 +02:00
|
|
|
|
const watchedProgress = this.getWatchedProgress()
|
2020-02-19 04:31:10 +01:00
|
|
|
|
this.activeFormat = 'dash'
|
|
|
|
|
this.hidePlayer = true
|
|
|
|
|
|
2020-02-28 04:29:30 +01:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.hidePlayer = false
|
2020-09-23 00:27:53 +02:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
if (player !== null) {
|
|
|
|
|
player.currentTime(watchedProgress)
|
|
|
|
|
}
|
|
|
|
|
}, 500)
|
2020-02-28 04:29:30 +01:00
|
|
|
|
}, 100)
|
2020-02-19 04:31:10 +01:00
|
|
|
|
},
|
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
|
enableLegacyFormat: function () {
|
2020-02-20 21:58:21 +01:00
|
|
|
|
if (this.activeFormat === 'legacy') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-23 00:27:53 +02:00
|
|
|
|
const watchedProgress = this.getWatchedProgress()
|
2020-02-19 04:31:10 +01:00
|
|
|
|
this.activeFormat = 'legacy'
|
2020-06-01 04:47:22 +02:00
|
|
|
|
this.activeSourceList = this.videoSourceList
|
|
|
|
|
this.hidePlayer = true
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.hidePlayer = false
|
2020-09-23 00:27:53 +02:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
if (player !== null) {
|
|
|
|
|
player.currentTime(watchedProgress)
|
|
|
|
|
}
|
|
|
|
|
}, 500)
|
2020-06-01 04:47:22 +02:00
|
|
|
|
}, 100)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
enableAudioFormat: function () {
|
|
|
|
|
if (this.activeFormat === 'audio') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-16 04:07:54 +02:00
|
|
|
|
if (this.audioSourceList === null) {
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Change Format.Audio formats are not available for this video'))
|
2020-09-16 04:07:54 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-23 00:27:53 +02:00
|
|
|
|
const watchedProgress = this.getWatchedProgress()
|
2020-06-01 04:47:22 +02:00
|
|
|
|
this.activeFormat = 'audio'
|
|
|
|
|
this.activeSourceList = this.audioSourceList
|
2020-02-19 04:31:10 +01:00
|
|
|
|
this.hidePlayer = true
|
|
|
|
|
|
2020-02-20 21:58:21 +01:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.hidePlayer = false
|
2020-09-23 00:27:53 +02:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
if (player !== null) {
|
|
|
|
|
player.currentTime(watchedProgress)
|
|
|
|
|
}
|
|
|
|
|
}, 500)
|
2020-02-20 21:58:21 +01:00
|
|
|
|
}, 100)
|
2020-02-21 19:31:32 +01:00
|
|
|
|
},
|
|
|
|
|
|
2020-05-17 22:12:58 +02:00
|
|
|
|
handleVideoEnded: function () {
|
2022-07-31 21:37:34 +02:00
|
|
|
|
if ((!this.watchingPlaylist || !this.autoplayPlaylists) && !this.playNextVideo) {
|
2021-04-29 20:33:13 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-06 17:25:50 +01:00
|
|
|
|
const nextVideoInterval = this.defaultInterval
|
2021-04-29 20:33:13 +02:00
|
|
|
|
this.playNextTimeout = setTimeout(() => {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
if (player !== null && player.paused()) {
|
|
|
|
|
if (this.watchingPlaylist) {
|
2021-03-06 21:09:13 +01:00
|
|
|
|
this.$refs.watchVideoPlaylist.playNextVideo()
|
2021-04-29 20:33:13 +02:00
|
|
|
|
} else {
|
2021-03-06 21:09:13 +01:00
|
|
|
|
const nextVideoId = this.recommendedVideos[0].videoId
|
2021-04-29 20:33:13 +02:00
|
|
|
|
this.$router.push({
|
|
|
|
|
path: `/watch/${nextVideoId}`
|
|
|
|
|
})
|
2022-10-14 07:59:49 +02:00
|
|
|
|
showToast(this.$t('Playing Next Video'))
|
2021-03-06 21:09:13 +01:00
|
|
|
|
}
|
2021-04-29 20:33:13 +02:00
|
|
|
|
}
|
|
|
|
|
}, nextVideoInterval * 1000)
|
|
|
|
|
|
|
|
|
|
let countDownTimeLeftInSecond = nextVideoInterval
|
|
|
|
|
const showCountDownMessage = () => {
|
|
|
|
|
// Will not display "Playing next video in no time" as it's too late to cancel
|
|
|
|
|
// Also there is a separate message when playing next video
|
|
|
|
|
if (countDownTimeLeftInSecond <= 0) {
|
2021-06-14 20:10:21 +02:00
|
|
|
|
clearInterval(this.playNextCountDownIntervalId)
|
2021-04-29 20:33:13 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2020-09-07 20:43:44 +02:00
|
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
|
// To avoid message flashing
|
|
|
|
|
// `time` is manually tested to be 700
|
|
|
|
|
const message = this.$tc('Playing Next Video Interval', countDownTimeLeftInSecond, { nextVideoInterval: countDownTimeLeftInSecond })
|
|
|
|
|
showToast(message, 700, () => {
|
|
|
|
|
clearTimeout(this.playNextTimeout)
|
|
|
|
|
clearInterval(this.playNextCountDownIntervalId)
|
|
|
|
|
showToast(this.$t('Canceled next video autoplay'))
|
2020-08-05 05:44:34 +02:00
|
|
|
|
})
|
2021-04-29 20:33:13 +02:00
|
|
|
|
|
|
|
|
|
// At least this var should be updated AFTER showing the message
|
|
|
|
|
countDownTimeLeftInSecond = countDownTimeLeftInSecond - 1
|
2020-05-17 22:12:58 +02:00
|
|
|
|
}
|
2021-04-29 20:33:13 +02:00
|
|
|
|
// Execute once before scheduling it
|
|
|
|
|
showCountDownMessage()
|
2021-06-14 20:10:21 +02:00
|
|
|
|
this.playNextCountDownIntervalId = setInterval(showCountDownMessage, 1000)
|
2020-05-17 22:12:58 +02:00
|
|
|
|
},
|
|
|
|
|
|
2022-06-21 04:33:42 +02:00
|
|
|
|
handleRouteChange: async function (videoId) {
|
|
|
|
|
// if the user navigates to another video, the ipc call for the userdata path
|
|
|
|
|
// takes long enough for the video id to have already changed to the new one
|
|
|
|
|
// receiving it as an arg instead of accessing it ourselves means we always have the right one
|
|
|
|
|
|
2020-09-22 00:39:25 +02:00
|
|
|
|
clearTimeout(this.playNextTimeout)
|
2021-06-14 20:10:21 +02:00
|
|
|
|
clearInterval(this.playNextCountDownIntervalId)
|
2022-09-29 22:01:54 +02:00
|
|
|
|
this.videoChapters = []
|
2020-09-22 00:39:25 +02:00
|
|
|
|
|
2021-04-21 20:38:46 +02:00
|
|
|
|
this.handleWatchProgress()
|
2021-01-21 22:20:42 +01:00
|
|
|
|
|
|
|
|
|
if (!this.isUpcoming && !this.isLoading) {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
2020-09-22 03:30:48 +02:00
|
|
|
|
|
2020-09-22 22:19:21 +02:00
|
|
|
|
if (player !== null && !player.paused() && player.isInPictureInPicture()) {
|
2020-09-22 03:30:48 +02:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
player.play()
|
2021-01-21 22:20:42 +01:00
|
|
|
|
player.on('leavepictureinpicture', (event) => {
|
2020-09-22 03:30:48 +02:00
|
|
|
|
const watchTime = player.currentTime()
|
|
|
|
|
if (this.$route.fullPath.includes('/watch')) {
|
|
|
|
|
const routeId = this.$route.params.id
|
2022-06-21 04:33:42 +02:00
|
|
|
|
if (routeId === videoId) {
|
2022-10-05 10:25:50 +02:00
|
|
|
|
this.$refs.videoPlayer.$refs.video.currentTime = watchTime
|
2020-09-22 03:30:48 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
player.pause()
|
|
|
|
|
player.dispose()
|
|
|
|
|
})
|
|
|
|
|
}, 200)
|
|
|
|
|
}
|
2020-09-07 20:43:44 +02:00
|
|
|
|
}
|
2021-04-01 15:54:45 +02:00
|
|
|
|
|
|
|
|
|
if (this.removeVideoMetaFiles) {
|
2022-11-05 15:44:21 +01:00
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
2022-06-21 04:33:42 +02:00
|
|
|
|
const dashFileLocation = `static/dashFiles/${videoId}.xml`
|
|
|
|
|
const vttFileLocation = `static/storyboards/${videoId}.vtt`
|
2021-04-01 15:54:45 +02:00
|
|
|
|
// only delete the file it actually exists
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (await pathExists(dashFileLocation)) {
|
|
|
|
|
await fs.rm(dashFileLocation)
|
2021-04-01 15:54:45 +02:00
|
|
|
|
}
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (await pathExists(vttFileLocation)) {
|
|
|
|
|
await fs.rm(vttFileLocation)
|
2021-04-01 15:54:45 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2022-12-06 12:37:44 +01:00
|
|
|
|
const userData = await getUserDataPath()
|
2022-06-21 04:33:42 +02:00
|
|
|
|
const dashFileLocation = `${userData}/dashFiles/${videoId}.xml`
|
|
|
|
|
const vttFileLocation = `${userData}/storyboards/${videoId}.vtt`
|
2021-04-01 15:54:45 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (await pathExists(dashFileLocation)) {
|
|
|
|
|
await fs.rm(dashFileLocation)
|
2021-04-01 15:54:45 +02:00
|
|
|
|
}
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (await pathExists(vttFileLocation)) {
|
|
|
|
|
await fs.rm(vttFileLocation)
|
2021-04-01 15:54:45 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-09-07 20:43:44 +02:00
|
|
|
|
},
|
|
|
|
|
|
2020-08-17 00:11:44 +02:00
|
|
|
|
handleVideoError: function (error) {
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.error(error)
|
2020-05-23 23:29:42 +02:00
|
|
|
|
if (this.isLive) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 19:31:32 +01:00
|
|
|
|
if (error.code === 4) {
|
|
|
|
|
if (this.activeFormat === 'dash') {
|
2022-09-23 03:04:10 +02:00
|
|
|
|
console.warn(
|
2020-02-28 04:29:30 +01:00
|
|
|
|
'Unable to play dash formats. Reverting to legacy formats...'
|
|
|
|
|
)
|
2020-02-21 19:31:32 +01:00
|
|
|
|
this.enableLegacyFormat()
|
|
|
|
|
} else {
|
|
|
|
|
this.enableDashFormat()
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-05 05:44:34 +02:00
|
|
|
|
},
|
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
|
createLocalDashManifest: async function (formats) {
|
2020-08-28 21:43:10 +02:00
|
|
|
|
const xmlData = ytDashGen.generate_dash_file_from_formats(formats, this.videoLengthSeconds)
|
2022-12-06 12:37:44 +01:00
|
|
|
|
const userData = await getUserDataPath()
|
2020-08-28 21:43:10 +02:00
|
|
|
|
let fileLocation
|
|
|
|
|
let uriSchema
|
2022-11-05 15:44:21 +01:00
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
2021-07-18 02:24:26 +02:00
|
|
|
|
fileLocation = `static/dashFiles/${this.videoId}.xml`
|
|
|
|
|
uriSchema = `dashFiles/${this.videoId}.xml`
|
2020-08-28 21:43:10 +02:00
|
|
|
|
// if the location does not exist, writeFileSync will not create the directory, so we have to do that manually
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (!(await pathExists('static/dashFiles/'))) {
|
|
|
|
|
await fs.mkdir('static/dashFiles/')
|
2020-08-28 21:43:10 +02:00
|
|
|
|
}
|
2021-07-18 02:24:26 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (await pathExists(fileLocation)) {
|
|
|
|
|
await fs.rm(fileLocation)
|
2021-09-23 09:04:00 +02:00
|
|
|
|
}
|
2022-12-29 02:19:48 +01:00
|
|
|
|
await fs.writeFile(fileLocation, xmlData)
|
2020-08-28 21:43:10 +02:00
|
|
|
|
} else {
|
|
|
|
|
fileLocation = `${userData}/dashFiles/${this.videoId}.xml`
|
|
|
|
|
uriSchema = `file://${fileLocation}`
|
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
if (!(await pathExists(`${userData}/dashFiles/`))) {
|
|
|
|
|
await fs.mkdir(`${userData}/dashFiles/`)
|
2020-08-28 21:43:10 +02:00
|
|
|
|
}
|
2021-07-18 02:24:26 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
await fs.writeFile(fileLocation, xmlData)
|
2020-08-28 21:43:10 +02:00
|
|
|
|
}
|
2021-07-18 02:24:26 +02:00
|
|
|
|
|
2020-08-28 21:43:10 +02:00
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
url: uriSchema,
|
|
|
|
|
type: 'application/dash+xml',
|
|
|
|
|
label: 'Dash',
|
|
|
|
|
qualityLabel: 'Auto'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
createInvidiousDashManifest: function () {
|
2021-10-12 23:03:22 +02:00
|
|
|
|
let url = `${this.currentInvidiousInstance}/api/manifest/dash/id/${this.videoId}`
|
2020-08-28 21:43:10 +02:00
|
|
|
|
|
2022-09-15 10:59:09 +02:00
|
|
|
|
if (this.proxyVideos || !process.env.IS_ELECTRON) {
|
2022-09-17 10:19:31 +02:00
|
|
|
|
url += '?local=true'
|
2020-08-28 21:43:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
url: url,
|
|
|
|
|
type: 'application/dash+xml',
|
|
|
|
|
label: 'Dash',
|
|
|
|
|
qualityLabel: 'Auto'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
createLocalStoryboardUrls: async function (templateUrl) {
|
2020-08-17 00:11:44 +02:00
|
|
|
|
const storyboards = templateUrl.split('|')
|
|
|
|
|
const storyboardArray = []
|
|
|
|
|
// Second storyboard: L1/M0 - Third storyboard: L2/M0 - Fourth: L3/M0
|
|
|
|
|
|
|
|
|
|
const baseUrl = storyboards.shift()
|
|
|
|
|
// remove the first link because it does not work
|
|
|
|
|
storyboards.splice(0, 1)
|
|
|
|
|
storyboards.forEach((storyboard, i) => {
|
|
|
|
|
// Not sure why the _ variable is needed, but storyboards don't work unless we initialize it.
|
|
|
|
|
|
|
|
|
|
/* eslint-disable-next-line */
|
|
|
|
|
const [width, height, count, sWidth, sHeight, interval, _, sigh] = storyboard.split('#')
|
|
|
|
|
storyboardArray.push({
|
|
|
|
|
url: baseUrl.replace('$L', i + 1).replace('$N', 'M0').replace(/<\/?sub>/g, '') + '&sigh=' + sigh,
|
|
|
|
|
width: Number(width), // Width of one sub image
|
|
|
|
|
height: Number(height), // Height of one sub image
|
|
|
|
|
sWidth: Number(sWidth), // Number of images vertically (if full)
|
|
|
|
|
sHeight: Number(sHeight), // Number of images horizontally (if full)
|
|
|
|
|
count: Number(count), // Number of images total
|
|
|
|
|
interval: Number(interval) // How long one image is used
|
|
|
|
|
})
|
|
|
|
|
})
|
2022-06-13 15:31:32 +02:00
|
|
|
|
// TODO: MAKE A VARIABLE WHICH CAN CHOOSE BETWEEN STORYBOARD ARRAY ELEMENTS
|
2022-10-10 09:45:18 +02:00
|
|
|
|
const results = buildVTTFileLocally(storyboardArray[1])
|
2022-12-29 02:19:48 +01:00
|
|
|
|
const userData = await getUserDataPath()
|
|
|
|
|
let fileLocation
|
|
|
|
|
let uriSchema
|
2021-07-18 02:24:26 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
// Dev mode doesn't have access to the file:// schema, so we access
|
|
|
|
|
// storyboards differently when run in dev
|
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
|
|
fileLocation = `static/storyboards/${this.videoId}.vtt`
|
|
|
|
|
uriSchema = `storyboards/${this.videoId}.vtt`
|
|
|
|
|
// if the location does not exist, writeFile will not create the directory, so we have to do that manually
|
|
|
|
|
if (!(await pathExists('static/storyboards/'))) {
|
|
|
|
|
fs.mkdir('static/storyboards/')
|
|
|
|
|
} else if (await pathExists(fileLocation)) {
|
|
|
|
|
await fs.rm(fileLocation)
|
|
|
|
|
}
|
2021-07-18 02:24:26 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
await fs.writeFile(fileLocation, results)
|
|
|
|
|
} else {
|
|
|
|
|
if (!(await pathExists(`${userData}/storyboards/`))) {
|
|
|
|
|
await fs.mkdir(`${userData}/storyboards/`)
|
2020-08-17 00:11:44 +02:00
|
|
|
|
}
|
2022-12-29 02:19:48 +01:00
|
|
|
|
fileLocation = `${userData}/storyboards/${this.videoId}.vtt`
|
|
|
|
|
uriSchema = `file://${fileLocation}`
|
2020-08-17 00:11:44 +02:00
|
|
|
|
|
2022-12-29 02:19:48 +01:00
|
|
|
|
await fs.writeFile(fileLocation, results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.videoStoryboardSrc = uriSchema
|
2020-08-17 00:11:44 +02:00
|
|
|
|
},
|
|
|
|
|
|
2021-03-19 20:18:21 +01:00
|
|
|
|
tryAddingTranslatedLocaleCaption: function (captionTracks, locale, baseUrl) {
|
2021-03-19 03:36:45 +01:00
|
|
|
|
const enCaptionIdx = captionTracks.findIndex(track =>
|
|
|
|
|
track.languageCode === 'en' && track.kind !== 'asr'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const enCaptionExists = enCaptionIdx !== -1
|
|
|
|
|
const asrEnabled = captionTracks.some(track => track.kind === 'asr')
|
|
|
|
|
|
|
|
|
|
if (enCaptionExists || asrEnabled) {
|
|
|
|
|
let label
|
|
|
|
|
let url
|
|
|
|
|
|
|
|
|
|
if (this.$te('Video.translated from English') && this.$t('Video.translated from English') !== '') {
|
|
|
|
|
label = `${this.$t('Locale Name')} (${this.$t('Video.translated from English')})`
|
|
|
|
|
} else {
|
|
|
|
|
label = `${this.$t('Locale Name')} (translated from English)`
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-27 04:46:49 +02:00
|
|
|
|
const indexTranslated = captionTracks.findIndex((item) => {
|
|
|
|
|
return item.name.simpleText === label
|
|
|
|
|
})
|
|
|
|
|
if (indexTranslated !== -1) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-19 03:36:45 +01:00
|
|
|
|
if (enCaptionExists) {
|
|
|
|
|
url = new URL(captionTracks[enCaptionIdx].baseUrl)
|
|
|
|
|
} else {
|
|
|
|
|
url = new URL(baseUrl)
|
|
|
|
|
url.searchParams.set('lang', 'en')
|
|
|
|
|
url.searchParams.set('kind', 'asr')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url.searchParams.set('tlang', locale)
|
|
|
|
|
captionTracks.unshift({
|
|
|
|
|
baseUrl: url.toString(),
|
|
|
|
|
name: { simpleText: label },
|
|
|
|
|
languageCode: locale
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-03-17 02:28:25 +01:00
|
|
|
|
createCaptionPromiseList: function (captionTracks) {
|
|
|
|
|
return captionTracks.map(caption => new Promise((resolve, reject) => {
|
2020-08-17 00:11:44 +02:00
|
|
|
|
caption.type = 'text/vtt'
|
|
|
|
|
caption.charset = 'charset=utf-8'
|
|
|
|
|
caption.dataSource = 'local'
|
|
|
|
|
|
2021-03-19 03:36:45 +01:00
|
|
|
|
const url = new URL(caption.baseUrl)
|
|
|
|
|
url.searchParams.set('fmt', 'vtt')
|
|
|
|
|
|
2022-09-21 09:05:03 +02:00
|
|
|
|
fetch(url)
|
|
|
|
|
.then((response) => response.text())
|
|
|
|
|
.then((text) => {
|
|
|
|
|
// The character '#' needs to be percent-encoded in a (data) URI
|
|
|
|
|
// because it signals an identifier, which means anything after it
|
|
|
|
|
// is automatically removed when the URI is used as a source
|
|
|
|
|
let vtt = text.replace(/#/g, '%23')
|
|
|
|
|
|
|
|
|
|
// A lot of videos have messed up caption positions that need to be removed
|
|
|
|
|
// This can be either because this format isn't really used by YouTube
|
|
|
|
|
// or because it's expected for the player to be able to somehow
|
|
|
|
|
// wrap the captions so that they won't step outside its boundaries
|
|
|
|
|
//
|
|
|
|
|
// Auto-generated captions are also all aligned to the start
|
|
|
|
|
// so those instances must also be removed
|
|
|
|
|
// In addition, all aligns seem to be fixed to "start" when they do pop up in normal captions
|
|
|
|
|
// If it's prominent enough that people start to notice, it can be removed then
|
|
|
|
|
if (caption.kind === 'asr') {
|
|
|
|
|
vtt = vtt.replace(/ align:start| position:\d{1,3}%/g, '')
|
|
|
|
|
} else {
|
|
|
|
|
vtt = vtt.replace(/ position:\d{1,3}%/g, '')
|
|
|
|
|
}
|
2021-03-17 02:28:25 +01:00
|
|
|
|
|
2022-09-21 09:05:03 +02:00
|
|
|
|
caption.baseUrl = `data:${caption.type};${caption.charset},${vtt}`
|
|
|
|
|
resolve(caption)
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(error)
|
|
|
|
|
reject(error)
|
|
|
|
|
})
|
2021-03-17 02:28:25 +01:00
|
|
|
|
}))
|
2020-08-17 00:11:44 +02:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-13 17:31:43 +02:00
|
|
|
|
pausePlayer: function () {
|
|
|
|
|
const player = this.$refs.videoPlayer.player
|
|
|
|
|
if (player && !player.paused()) {
|
|
|
|
|
player.pause()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-11 05:48:06 +02:00
|
|
|
|
getWatchedProgress: function () {
|
|
|
|
|
return this.$refs.videoPlayer && this.$refs.videoPlayer.player ? this.$refs.videoPlayer.player.currentTime() : 0
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getTimestamp: function () {
|
|
|
|
|
return Math.floor(this.getWatchedProgress())
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-13 17:31:43 +02:00
|
|
|
|
getPlaylistIndex: function () {
|
|
|
|
|
return this.$refs.watchVideoPlaylist
|
|
|
|
|
? this.getPlaylistReverse()
|
|
|
|
|
? this.$refs.watchVideoPlaylist.playlistItems.length - this.$refs.watchVideoPlaylist.currentVideoIndex
|
|
|
|
|
: this.$refs.watchVideoPlaylist.currentVideoIndex - 1
|
|
|
|
|
: -1
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getPlaylistReverse: function () {
|
|
|
|
|
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.reversePlaylist : false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getPlaylistShuffle: function () {
|
|
|
|
|
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.shuffleEnabled : false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getPlaylistLoop: function () {
|
|
|
|
|
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.loopEnabled : false
|
|
|
|
|
},
|
|
|
|
|
|
2020-10-31 15:36:35 +01:00
|
|
|
|
updateTitle: function () {
|
|
|
|
|
document.title = `${this.videoTitle} - FreeTube`
|
|
|
|
|
},
|
|
|
|
|
|
2020-08-05 05:44:34 +02:00
|
|
|
|
...mapActions([
|
2020-08-20 04:39:44 +02:00
|
|
|
|
'updateHistory',
|
2021-05-22 01:49:48 +02:00
|
|
|
|
'updateWatchProgress',
|
2021-05-22 01:56:32 +02:00
|
|
|
|
'ytGetVideoInformation',
|
2022-06-03 14:52:35 +02:00
|
|
|
|
'invidiousGetVideoInformation',
|
2022-10-18 10:15:28 +02:00
|
|
|
|
'updateSubscriptionDetails'
|
2020-08-05 05:44:34 +02:00
|
|
|
|
])
|
2020-08-05 04:18:39 +02:00
|
|
|
|
}
|
2020-02-16 19:30:00 +01:00
|
|
|
|
})
|