Add Live tab to channel pages (#3273)

* Add Live tab to channel pages

* Handle invidious streams tab URL

* Clear live videos when navigating between channels

* Reset sort by when changing channels
This commit is contained in:
absidue 2023-03-15 07:37:52 +01:00 committed by GitHub
parent a878ff395a
commit 0212467419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 15 deletions

View File

@ -315,7 +315,7 @@ const actions = {
let urlType = 'unknown'
const channelPattern =
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|playlists|about|community|channels))?\/?$/
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|live|streams|playlists|about|community|channels))?\/?$/
const typePatterns = new Map([
['playlist', /^(\/playlist\/?|\/embed(\/?videoseries)?)$/],
@ -421,6 +421,10 @@ const actions = {
let subPath = null
switch (match.groups.tab) {
case 'live':
case 'streams':
subPath = 'live'
break
case 'playlists':
subPath = 'playlists'
break

View File

@ -59,6 +59,7 @@ export default defineComponent({
subCount: 0,
searchPage: 2,
videoContinuationData: null,
liveContinuationData: null,
playlistContinuationData: null,
searchContinuationData: null,
communityContinuationData: null,
@ -68,10 +69,12 @@ export default defineComponent({
joined: 0,
location: null,
videoSortBy: 'newest',
liveSortBy: 'newest',
playlistSortBy: 'newest',
lastSearchQuery: '',
relatedChannels: [],
latestVideos: [],
latestLive: [],
latestPlaylists: [],
latestCommunityPosts: [],
searchResults: [],
@ -81,19 +84,13 @@ export default defineComponent({
errorMessage: '',
showSearchBar: true,
showShareMenu: true,
videoSelectValues: [
videoShortLiveSelectValues: [
'newest',
'popular'
],
playlistSelectValues: [
'newest',
'last'
],
tabInfoValues: [
'videos',
'playlists',
'community',
'about'
]
}
},
@ -152,7 +149,7 @@ export default defineComponent({
}
},
videoSelectNames: function () {
videoShortLiveSelectNames: function () {
return [
this.$t('Channel.Videos.Sort Types.Newest'),
this.$t('Channel.Videos.Sort Types.Most Popular')
@ -185,6 +182,8 @@ export default defineComponent({
switch (this.currentTab) {
case 'videos':
return !isNullOrEmpty(this.videoContinuationData)
case 'live':
return !isNullOrEmpty(this.liveContinuationData)
case 'playlists':
return !isNullOrEmpty(this.playlistContinuationData)
case 'community':
@ -209,6 +208,28 @@ export default defineComponent({
hideSharingActions: function () {
return this.$store.getters.getHideSharingActions
},
hideLiveStreams: function () {
return this.$store.getters.getHideLiveStreams
},
tabInfoValues: function () {
const values = [
'videos',
'live',
'playlists',
'community',
'about'
]
// remove tabs from the array based on user settings
if (this.hideLiveStreams) {
const index = values.indexOf('live')
values.splice(index, 1)
}
return values
}
},
watch: {
@ -222,21 +243,30 @@ export default defineComponent({
}
this.id = this.$route.params.id
this.currentTab = this.$route.params.currentTab ?? 'videos'
let currentTab = this.$route.params.currentTab ?? 'videos'
this.searchPage = 2
this.relatedChannels = []
this.latestVideos = []
this.latestLive = []
this.liveSortBy = 'newest'
this.latestPlaylists = []
this.searchResults = []
this.shownElementList = []
this.apiUsed = ''
this.channelInstance = ''
this.videoContinuationData = null
this.liveContinuationData = null
this.playlistContinuationData = null
this.searchContinuationData = null
this.communityContinuationData = null
this.showSearchBar = true
if (this.hideLiveStreams && currentTab === 'live') {
currentTab = 'videos'
}
this.currentTab = currentTab
if (this.id === '@@@') {
this.showShareMenu = false
this.setErrorMessage(this.$i18n.t('Channel.This channel does not exist'))
@ -268,6 +298,21 @@ export default defineComponent({
}
},
liveSortBy () {
this.isElementListLoading = true
this.latestLive = []
switch (this.apiUsed) {
case 'local':
this.getChannelLiveLocal()
break
case 'invidious':
this.channelInvidiousLive(true)
break
default:
this.getChannelLiveLocal()
}
},
playlistSortBy () {
this.isElementListLoading = true
this.latestPlaylists = []
@ -293,7 +338,14 @@ export default defineComponent({
}
this.id = this.$route.params.id
this.currentTab = this.$route.params.currentTab ?? 'videos'
let currentTab = this.$route.params.currentTab ?? 'videos'
if (this.hideLiveStreams && currentTab === 'live') {
currentTab = 'videos'
}
this.currentTab = currentTab
if (this.id === '@@@') {
this.showShareMenu = false
@ -504,6 +556,10 @@ export default defineComponent({
this.getChannelVideosLocal()
}
if (!this.hideLiveStreams && channel.has_live_streams) {
this.getChannelLiveLocal()
}
if (channel.has_playlists) {
this.getChannelPlaylistsLocal()
}
@ -573,7 +629,7 @@ export default defineComponent({
let videosTab = await channel.getVideos()
if (this.videoSortBy !== 'newest') {
const index = this.videoSelectValues.indexOf(this.videoSortBy)
const index = this.videoShortLiveSelectValues.indexOf(this.videoSortBy)
videosTab = await videosTab.applyFilter(videosTab.filters[index])
}
@ -617,6 +673,62 @@ export default defineComponent({
}
},
getChannelLiveLocal: async function () {
this.isElementListLoading = true
const expectedId = this.id
try {
/**
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').default}
*/
const channel = this.channelInstance
let liveTab = await channel.getLiveStreams()
if (this.liveSortBy !== 'newest') {
const index = this.videoShortLiveSelectValues.indexOf(this.liveSortBy)
liveTab = await liveTab.applyFilter(liveTab.filters[index])
}
if (expectedId !== this.id) {
return
}
this.latestLive = parseLocalChannelVideos(liveTab.videos, channel.header.author)
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
this.isElementListLoading = false
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (this.backendPreference === 'local' && this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
this.getChannelInfoInvidious()
} else {
this.isLoading = false
}
}
},
getChannelLiveLocalMore: async function () {
try {
/**
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').ChannelListContinuation|import('youtubei.js/dist/src/parser/youtube/Channel').FilteredChannelList}
*/
const continuation = await this.liveContinuationData.getContinuation()
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
this.liveContinuationData = continuation.has_continuation ? continuation : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
}
},
getChannelInfoInvidious: function () {
this.isLoading = true
this.apiUsed = 'invidious'
@ -670,6 +782,10 @@ export default defineComponent({
this.channelInvidiousVideos()
}
if (!this.hideLiveStreams && response.tabs.includes('streams')) {
this.channelInvidiousLive()
}
if (response.tabs.includes('playlists')) {
this.getPlaylistsInvidious()
}
@ -735,6 +851,47 @@ export default defineComponent({
})
},
channelInvidiousLive: function (sortByChanged) {
const payload = {
resource: 'channels',
id: this.id,
subResource: 'streams',
params: {
sort_by: this.liveSortBy,
}
}
if (sortByChanged) {
this.liveContinuationData = null
}
let more = false
if (this.liveContinuationData) {
payload.params.continuation = this.liveContinuationData
more = true
}
if (!more) {
this.isElementListLoading = true
}
invidiousAPICall(payload).then((response) => {
if (more) {
this.latestLive.push(...response.videos)
} else {
this.latestLive = response.videos
}
this.liveContinuationData = response.continuation || null
this.isElementListLoading = false
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
})
},
getChannelPlaylistsLocal: async function () {
const expectedId = this.id
@ -1025,6 +1182,16 @@ export default defineComponent({
break
}
break
case 'live':
switch (this.apiUsed) {
case 'local':
this.getChannelLiveLocalMore()
break
case 'invidious':
this.channelInvidiousLive()
break
}
break
case 'playlists':
switch (this.apiUsed) {
case 'local':

View File

@ -100,6 +100,20 @@
>
{{ $t("Channel.Videos.Videos").toUpperCase() }}
</div>
<div
v-if="!hideLiveStreams"
id="liveTab"
class="tab"
:class="(currentTab==='live')?'selectedTab':''"
role="tab"
aria-selected="true"
aria-controls="livePanel"
tabindex="0"
@click="changeTab('live')"
@keydown.left.right.enter.space="changeTab('live', $event)"
>
{{ $t("Channel.Live.Live").toUpperCase() }}
</div>
<div
id="playlistsTab"
class="tab"
@ -265,12 +279,22 @@
<ft-select
v-show="currentTab === 'videos' && latestVideos.length > 0"
class="sortSelect"
:value="videoSelectValues[0]"
:select-names="videoSelectNames"
:select-values="videoSelectValues"
:value="videoShortLiveSelectValues[0]"
:select-names="videoShortLiveSelectNames"
:select-values="videoShortLiveSelectValues"
:placeholder="$t('Search Filters.Sort By.Sort By')"
@change="videoSortBy = $event"
/>
<ft-select
v-if="!hideLiveStreams"
v-show="currentTab === 'live' && latestLive.length > 0"
class="sortSelect"
:value="videoShortLiveSelectValues[0]"
:select-names="videoShortLiveSelectNames"
:select-values="videoShortLiveSelectValues"
:placeholder="$t('Search Filters.Sort By.Sort By')"
@change="liveSortBy = $event"
/>
<ft-select
v-show="currentTab === 'playlists' && latestPlaylists.length > 0"
class="sortSelect"
@ -301,6 +325,21 @@
{{ $t("Channel.Videos.This channel does not currently have any videos") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideLiveStreams"
v-show="currentTab === 'live'"
id="livePanel"
:data="latestLive"
role="tabpanel"
aria-labelledby="liveTab"
/>
<ft-flex-box
v-if="!hideLiveStreams && currentTab === 'live' && latestLive.length === 0"
>
<p class="message">
{{ $t("Channel.Live.This channel does not currently have any live streams") }}
</p>
</ft-flex-box>
<ft-element-list
v-show="currentTab === 'playlists'"
id="playlistPanel"

View File

@ -528,6 +528,10 @@ Channel:
Newest: Newest
Oldest: Oldest
Most Popular: Most Popular
Live:
Live: Live
This channel does not currently have any live streams: This channel does not currently
have any live streams
Playlists:
Playlists: Playlists
This channel does not currently have any playlists: This channel does not currently