From 7a4c8241d9b0ed7779f42bf9c43455ea2a96aabe Mon Sep 17 00:00:00 2001
From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Date: Sat, 25 Jan 2025 14:33:47 -0500
Subject: [PATCH] Add support for channel's `Courses` tab
---
.../ChannelDetails/ChannelDetails.vue | 16 ++
.../distraction-settings.js | 4 +
.../distraction-settings.vue | 6 +
src/renderer/helpers/api/invidious.js | 11 +-
src/renderer/store/modules/settings.js | 1 +
src/renderer/store/modules/utils.js | 5 +-
src/renderer/views/Channel/Channel.js | 147 +++++++++++++++++-
src/renderer/views/Channel/Channel.vue | 15 ++
static/locales/en-US.yaml | 4 +
9 files changed, 204 insertions(+), 5 deletions(-)
diff --git a/src/renderer/components/ChannelDetails/ChannelDetails.vue b/src/renderer/components/ChannelDetails/ChannelDetails.vue
index adb685333529a..0aba9517da9a4 100644
--- a/src/renderer/components/ChannelDetails/ChannelDetails.vue
+++ b/src/renderer/components/ChannelDetails/ChannelDetails.vue
@@ -171,6 +171,22 @@
{{ $t("Channel.Podcasts.Podcasts").toUpperCase() }}
+
+ {{ $t("Channel.Courses.Courses").toUpperCase() }}
+
+
+
}
@@ -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
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index 4ee32fd53bf73..2f1fc4bafb853 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -186,6 +186,7 @@ const state = {
hideChannelPlaylists: false,
hideChannelReleases: false,
hideChannelPodcasts: false,
+ hideChannelCourses: false,
hideChannelShorts: false,
hideChannelSubscriptions: false,
hideCommentLikes: false,
diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js
index e5b42ecb6d5dd..64541069d7166 100644
--- a/src/renderer/store/modules/utils.js
+++ b/src/renderer/store/modules/utils.js
@@ -475,7 +475,7 @@ const actions = {
let urlType = 'unknown'
const channelPattern =
- /^\/(?:(?:channel|user|c)\/)?(?[^/]+)(?:\/(?join|featured|videos|shorts|live|streams|podcasts|releases|playlists|about|community|channels))?\/?$/
+ /^\/(?:(?:channel|user|c)\/)?(?[^/]+)(?:\/(?join|featured|videos|shorts|live|streams|podcasts|releases|courses|playlists|about|community|channels))?\/?$/
const hashtagPattern = /^\/hashtag\/(?[^#&/?]+)$/
@@ -622,6 +622,9 @@ const actions = {
case 'podcasts':
subPath = 'podcasts'
break
+ case 'courses':
+ subPath = 'courses'
+ break
case 'releases':
subPath = 'releases'
break
diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js
index 4e7d83336ff15..bb340d6b61d8e 100644
--- a/src/renderer/views/Channel/Channel.js
+++ b/src/renderer/views/Channel/Channel.js
@@ -26,6 +26,7 @@ import {
getInvidiousChannelPlaylists,
getInvidiousChannelPodcasts,
getInvidiousChannelReleases,
+ getInvidiousChannelCourses,
getInvidiousChannelShorts,
getInvidiousChannelVideos,
invidiousGetChannelId,
@@ -87,6 +88,7 @@ export default defineComponent({
liveContinuationData: null,
releaseContinuationData: null,
podcastContinuationData: null,
+ coursesContinuationData: null,
playlistContinuationData: null,
searchContinuationData: null,
communityContinuationData: null,
@@ -112,6 +114,7 @@ export default defineComponent({
latestLive: [],
latestReleases: [],
latestPodcasts: [],
+ latestCourses: [],
latestPlaylists: [],
latestCommunityPosts: [],
searchResults: [],
@@ -134,6 +137,7 @@ export default defineComponent({
'live',
'releases',
'podcasts',
+ 'courses',
'playlists',
'community',
'about'
@@ -144,6 +148,7 @@ export default defineComponent({
'live',
'releases',
'podcasts',
+ 'courses',
'playlists',
'community',
'about'
@@ -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':
@@ -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
},
@@ -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'))
}
@@ -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')
@@ -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()
}
@@ -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)')
@@ -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
@@ -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) {
diff --git a/src/renderer/views/Channel/Channel.vue b/src/renderer/views/Channel/Channel.vue
index 660138cb480b6..b04041f6a6729 100644
--- a/src/renderer/views/Channel/Channel.vue
+++ b/src/renderer/views/Channel/Channel.vue
@@ -170,6 +170,21 @@
{{ $t("Channel.Releases.This channel does not currently have any releases") }}
+
+
+
+ {{ $t("Channel.Courses.This channel does not currently have any courses") }}
+
+