Skip to content

Commit

Permalink
Add support for channel's Courses tab
Browse files Browse the repository at this point in the history
  • Loading branch information
ChunkyProgrammer committed Feb 10, 2025
1 parent 83d99dd commit 7a4c824
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 5 deletions.
16 changes: 16 additions & 0 deletions src/renderer/components/ChannelDetails/ChannelDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,22 @@
{{ $t("Channel.Podcasts.Podcasts").toUpperCase() }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
v-if="visibleTabs.includes('courses')"
id="coursesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'courses')"
aria-controls="coursesPanel"
:tabindex="currentTab === 'courses' ? 0 : -1"
:class="{ selectedTab: currentTab === 'courses' }"
@click="changeTab('courses')"
@keydown.left.right="focusTab('courses', $event)"
@keydown.enter.space.prevent="changeTab('courses')"
>
{{ $t("Channel.Courses.Courses").toUpperCase() }}
</div>
<!-- eslint-disable-next-line vuejs-accessibility/interactive-supports-focus -->
<div
v-if="visibleTabs.includes('playlists')"
id="playlistsTab"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export default defineComponent({
hideChannelReleases: function () {
return this.$store.getters.getHideChannelReleases
},
hideChannelCourses: function () {
return this.$store.getters.getHideChannelCourses
},
hideChannelCommunity: function () {
return this.$store.getters.getHideChannelCommunity
},
Expand Down Expand Up @@ -235,6 +238,7 @@ export default defineComponent({
'updateHideChannelHome',
'updateHideChannelPodcasts',
'updateHideChannelReleases',
'updateHideChannelCourses',
'updateHideSubscriptionsVideos',
'updateHideSubscriptionsShorts',
'updateHideSubscriptionsLive',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@
:default-value="hideChannelReleases"
@change="updateHideChannelReleases"
/>
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Channel Courses')"
:compact="true"
:default-value="hideChannelCourses"
@change="updateHideChannelCourses"
/>
</div>
</div>
<h4
Expand Down
11 changes: 10 additions & 1 deletion src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export async function invidiousGetChannelId(url) {
* description: string,
* descriptionHtml: string,
* allowedRegions: string[],
* tabs: ('home' | 'videos' | 'shorts' | 'streams' | 'podcasts' | 'releases' | 'playlists' | 'community')[],
* tabs: ('home' | 'videos' | 'shorts' | 'streams' | 'podcasts' | 'releases' | 'courses' | 'playlists' | 'community')[],
* latestVideos: InvidiousVideoType[],
* relatedChannels: InvidiousChannelObject[]
* }>}
Expand Down Expand Up @@ -240,6 +240,15 @@ export async function getInvidiousChannelPodcasts(channelId, continuation) {
return await getInvidiousChannelTab('podcasts', channelId, continuation)
}

/**
* @param {string} channelId
* @param {string | undefined | null} continuation
*/
export async function getInvidiousChannelCourses(channelId, continuation) {
/** @type {{continuation: string?, playlists: InvidiousPlaylistObject[]}} */
return await getInvidiousChannelTab('courses', channelId, continuation)
}

/**
* @param {string} channelId
* @param {string} query
Expand Down
1 change: 1 addition & 0 deletions src/renderer/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ const state = {
hideChannelPlaylists: false,
hideChannelReleases: false,
hideChannelPodcasts: false,
hideChannelCourses: false,
hideChannelShorts: false,
hideChannelSubscriptions: false,
hideCommentLikes: false,
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/store/modules/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ const actions = {
let urlType = 'unknown'

const channelPattern =
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|podcasts|releases|playlists|about|community|channels))?\/?$/
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|podcasts|releases|courses|playlists|about|community|channels))?\/?$/

const hashtagPattern = /^\/hashtag\/(?<tag>[^#&/?]+)$/

Expand Down Expand Up @@ -622,6 +622,9 @@ const actions = {
case 'podcasts':
subPath = 'podcasts'
break
case 'courses':
subPath = 'courses'
break
case 'releases':
subPath = 'releases'
break
Expand Down
147 changes: 144 additions & 3 deletions src/renderer/views/Channel/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getInvidiousChannelPlaylists,
getInvidiousChannelPodcasts,
getInvidiousChannelReleases,
getInvidiousChannelCourses,
getInvidiousChannelShorts,
getInvidiousChannelVideos,
invidiousGetChannelId,
Expand Down Expand Up @@ -87,6 +88,7 @@ export default defineComponent({
liveContinuationData: null,
releaseContinuationData: null,
podcastContinuationData: null,
coursesContinuationData: null,
playlistContinuationData: null,
searchContinuationData: null,
communityContinuationData: null,
Expand All @@ -112,6 +114,7 @@ export default defineComponent({
latestLive: [],
latestReleases: [],
latestPodcasts: [],
latestCourses: [],
latestPlaylists: [],
latestCommunityPosts: [],
searchResults: [],
Expand All @@ -134,6 +137,7 @@ export default defineComponent({
'live',
'releases',
'podcasts',
'courses',
'playlists',
'community',
'about'
Expand All @@ -144,6 +148,7 @@ export default defineComponent({
'live',
'releases',
'podcasts',
'courses',
'playlists',
'community',
'about'
Expand Down Expand Up @@ -234,6 +239,8 @@ export default defineComponent({
return !isNullOrEmpty(this.releaseContinuationData)
case 'podcasts':
return !isNullOrEmpty(this.podcastContinuationData)
case 'courses':
return !isNullOrEmpty(this.coursesContinuationData)
case 'playlists':
return !isNullOrEmpty(this.playlistContinuationData)
case 'community':
Expand Down Expand Up @@ -261,6 +268,10 @@ export default defineComponent({
return this.$store.getters.getHideChannelReleases
},

hideChannelCourses: function() {
return this.$store.getters.getHideChannelCourses
},

hideChannelPlaylists: function() {
return this.$store.getters.getHideChannelPlaylists
},
Expand Down Expand Up @@ -302,6 +313,10 @@ export default defineComponent({
indexToRemove.push(values.indexOf('releases'))
}

if (this.hideChannelCourses) {
indexToRemove.push(values.indexOf('courses'))
}

if (this.hideChannelHome) {
indexToRemove.push(values.indexOf('home'))
}
Expand Down Expand Up @@ -668,6 +683,11 @@ export default defineComponent({
this.getChannelReleasesLocal()
}

if (!this.hideChannelCourses && channel.has_courses) {
tabs.push('courses')
this.getChannelCoursesLocal()
}

if (!this.hideChannelPlaylists) {
if (channel.has_playlists) {
tabs.push('playlists')
Expand Down Expand Up @@ -1126,6 +1146,10 @@ export default defineComponent({
this.channelInvidiousReleases()
}

if (!this.hideChannelCourses && response.tabs.includes('courses')) {
this.channelInvidiousCourses()
}

if (!this.hideChannelPlaylists && response.tabs.includes('playlists')) {
this.getPlaylistsInvidious()
}
Expand Down Expand Up @@ -1560,7 +1584,7 @@ export default defineComponent({

const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts)
this.releaseContinuationData = continuation.has_continuation ? continuation : null
this.podcastContinuationData = continuation.has_continuation ? continuation : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
Expand Down Expand Up @@ -1620,6 +1644,108 @@ export default defineComponent({
})
},

getChannelCoursesLocal: async function () {
this.isElementListLoading = true
const expectedId = this.id

try {
/**
* @type {import('youtubei.js').YT.Channel}
*/
const channel = this.channelInstance
const coursesTab = await channel.getCourses()

if (expectedId !== this.id) {
return
}

this.latestCourses = coursesTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.coursesContinuationData = coursesTab.has_continuation ? coursesTab : 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.channelInvidiousCourses()
} else {
this.isLoading = false
}
}
},

getChannelCoursesLocalMore: async function () {
try {
/**
* @type {import('youtubei.js').YT.ChannelListContinuation}
*/
const continuation = await this.coursesContinuationData.getContinuation()

const parsedCourses = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.latestCourses = this.latestCourses.concat(parsedCourses)
this.coursesContinuationData = 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)
})
}
},

channelInvidiousCourses: function() {
this.isElementListLoading = true

getInvidiousChannelCourses(this.id).then((response) => {
this.coursesContinuationData = response.continuation || null
this.latestCourses = response.playlists
this.isElementListLoading = false
}).catch(async (err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
if (!this.channelInstance) {
this.channelInstance = await getLocalChannel(this.id)
}
this.getChannelCoursesLocal()
} else {
this.isLoading = false
}
})
},

channelInvidiousCoursesMore: function () {
if (this.coursesContinuationData === null) {
console.warn('There are no more courses available for this channel')
return
}

getInvidiousChannelCourses(this.id, this.coursesContinuationData).then((response) => {
this.coursesContinuationData = response.continuation || null
this.latestCourses = this.latestCourses.concat(response.playlists)
this.isElementListLoading = false
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
this.getChannelLocal()
} else {
this.isLoading = false
}
})
},

getCommunityPostsLocal: async function () {
const expectedId = this.id

Expand Down Expand Up @@ -1780,10 +1906,25 @@ export default defineComponent({
}
break
case 'releases':
this.getChannelReleasesLocalMore()
if (this.apiUsed === 'local') {
this.getChannelReleasesLocalMore()
} else {
this.channelInvidiousReleasesMore()
}
break
case 'podcasts':
this.getChannelPodcastsLocalMore()
if (this.apiUsed === 'local') {
this.getChannelPodcastsLocalMore()
} else {
this.channelInvidiousPodcastsMore()
}
break
case 'courses':
if (this.apiUsed === 'local') {
this.getChannelCoursesLocalMore()
} else {
this.channelInvidiousCoursesMore()
}
break
case 'playlists':
switch (this.apiUsed) {
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/views/Channel/Channel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@
{{ $t("Channel.Releases.This channel does not currently have any releases") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideChannelCourses && currentTab === 'courses'"
id="coursesPanel"
:data="latestCourses"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="coursesTab"
/>
<ft-flex-box
v-if="!hideChannelCourses && currentTab === 'courses' && latestCourses.length === 0"
>
<p class="message">
{{ $t("Channel.Courses.This channel does not currently have any courses") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideChannelPlaylists && currentTab === 'playlists'"
id="playlistPanel"
Expand Down
4 changes: 4 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ Settings:
Hide Channel Shorts: Hide Channel "Shorts" Tab
Hide Channel Podcasts: Hide Channel "Podcasts" Tab
Hide Channel Releases: Hide Channel "Releases" Tab
Hide Channel Courses: Hide Channel "Courses" Tab
Hide Videos and Playlists Containing Text: Hide Videos and Playlists Containing Text
Hide Videos and Playlists Containing Text Placeholder: Word, Word Fragment, or Phrase
Hide Subscriptions Videos: Hide Subscriptions Videos
Expand Down Expand Up @@ -780,6 +781,9 @@ Channel:
Releases:
Releases: Releases
This channel does not currently have any releases: This channel does not currently have any releases
Courses:
Courses: Courses
This channel does not currently have any courses: This channel does not currently have any courses
About:
About: About
Channel Description: Channel Description
Expand Down

0 comments on commit 7a4c824

Please sign in to comment.