} */
const popularVisible = computed(() => {
return !store.getters.getHidePopularVideos &&
- (store.getters.getBackendFallback || store.getters.getBackendPreference === 'invidious')
+ ((store.getters.getBackendFallback && store.getters.getFallbackPreference) || store.getters.getBackendPreference === 'invidious')
})
const applyNavIconExpand = computed(() => {
diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js
index 599595a839cbc..98871689ef921 100644
--- a/src/renderer/components/ft-list-video/ft-list-video.js
+++ b/src/renderer/components/ft-list-video/ft-list-video.js
@@ -159,6 +159,10 @@ export default defineComponent({
return this.$store.getters.getBackendPreference
},
+ fallbackPreference: function () {
+ return this.$store.getters.getFallbackPreference
+ },
+
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
},
@@ -345,23 +349,40 @@ export default defineComponent({
return this.deArrowCache.thumbnail
}
- let baseUrl
- if (this.backendPreference === 'invidious') {
+ let baseUrl = ''
+ let backendPreference = this.backendPreference
+ if (backendPreference === 'piped') {
+ if (this.data.thumbnail) {
+ return this.data.thumbnail
+ } else {
+ // this should be removed once piped supports more endpoints
+ backendPreference = this.fallbackPreference
+ }
+ }
+
+ if (!process.env.SUPPORTS_LOCAL_API || backendPreference === 'invidious') {
baseUrl = this.currentInvidiousInstanceUrl
} else {
baseUrl = 'https://i.ytimg.com'
}
+ let imageUrl = ''
+
switch (this.thumbnailPreference) {
case 'start':
- return `${baseUrl}/vi/${this.id}/mq1.jpg`
+ imageUrl = `${baseUrl}/vi/${this.id}/mq1.jpg`
+ break
case 'middle':
- return `${baseUrl}/vi/${this.id}/mq2.jpg`
+ imageUrl = `${baseUrl}/vi/${this.id}/mq2.jpg`
+ break
case 'end':
- return `${baseUrl}/vi/${this.id}/mq3.jpg`
+ imageUrl = `${baseUrl}/vi/${this.id}/mq3.jpg`
+ break
default:
- return `${baseUrl}/vi/${this.id}/mqdefault.jpg`
+ imageUrl = `${baseUrl}/vi/${this.id}/mqdefault.jpg`
}
+
+ return imageUrl
},
hideVideoViews: function () {
diff --git a/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js b/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js
index 96cefa2692b3f..772f3d7f8e235 100644
--- a/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js
+++ b/src/renderer/components/ft-profile-channel-list/ft-profile-channel-list.js
@@ -41,7 +41,11 @@ export default defineComponent({
},
computed: {
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
diff --git a/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js b/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js
index b18f57b70a156..78cc6359966bf 100644
--- a/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js
+++ b/src/renderer/components/ft-profile-filter-channels-list/ft-profile-filter-channels-list.js
@@ -34,7 +34,11 @@ export default defineComponent({
},
computed: {
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
diff --git a/src/renderer/components/general-settings/general-settings.js b/src/renderer/components/general-settings/general-settings.js
index c8e089f3967ac..9d41be97617eb 100644
--- a/src/renderer/components/general-settings/general-settings.js
+++ b/src/renderer/components/general-settings/general-settings.js
@@ -2,10 +2,8 @@ import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import FtSettingsSection from '../FtSettingsSection/FtSettingsSection.vue'
import FtSelect from '../ft-select/ft-select.vue'
-import FtInput from '../ft-input/ft-input.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
-import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
-import FtButton from '../ft-button/ft-button.vue'
+import FtInstanceSelector from '../FtInstanceSelector/FtInstanceSelector.vue'
import debounce from 'lodash.debounce'
import allLocales from '../../../../static/locales/activeLocales.json'
@@ -17,20 +15,20 @@ export default defineComponent({
components: {
'ft-settings-section': FtSettingsSection,
'ft-select': FtSelect,
- 'ft-input': FtInput,
'ft-toggle-switch': FtToggleSwitch,
- 'ft-flex-box': FtFlexBox,
- 'ft-button': FtButton
+ FtInstanceSelector
},
data: function () {
return {
backendValues: process.env.SUPPORTS_LOCAL_API
? [
'invidious',
- 'local'
+ 'local',
+ 'piped'
]
: [
- 'invidious'
+ 'invidious',
+ 'piped'
],
viewTypeValues: [
'grid',
@@ -61,6 +59,9 @@ export default defineComponent({
}
},
computed: {
+ currentPipedInstance: function () {
+ return this.$store.getters.getCurrentPipedInstance
+ },
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
@@ -70,6 +71,9 @@ export default defineComponent({
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
+ fallbackPreference: function () {
+ return this.$store.getters.getFallbackPreference
+ },
blurThumbnails: function () {
return this.$store.getters.getBlurThumbnails
},
@@ -141,6 +145,12 @@ export default defineComponent({
defaultInvidiousInstance: function () {
return this.$store.getters.getDefaultInvidiousInstance
},
+ pipedInstancesList: function () {
+ return this.$store.getters.getPipedInstancesList
+ },
+ defaultPipedInstance: function () {
+ return this.$store.getters.getDefaultPipedInstance
+ },
generalAutoLoadMorePaginatedItemsEnabled() {
return this.$store.getters.getGeneralAutoLoadMorePaginatedItemsEnabled
},
@@ -163,11 +173,13 @@ export default defineComponent({
if (process.env.SUPPORTS_LOCAL_API) {
return [
this.$t('Settings.General Settings.Preferred API Backend.Invidious API'),
- this.$t('Settings.General Settings.Preferred API Backend.Local API')
+ this.$t('Settings.General Settings.Preferred API Backend.Local API'),
+ this.$t('Settings.General Settings.Preferred API Backend.Piped API')
]
} else {
return [
- this.$t('Settings.General Settings.Preferred API Backend.Invidious API')
+ this.$t('Settings.General Settings.Preferred API Backend.Invidious API'),
+ this.$t('Settings.General Settings.Preferred API Backend.Piped API')
]
}
},
@@ -209,17 +221,26 @@ export default defineComponent({
created: function () {
this.setCurrentInvidiousInstanceBounce =
debounce(this.setCurrentInvidiousInstance, 500)
+
+ this.setCurrentPipedInstanceBounce =
+ debounce(this.setCurrentPipedInstance, 500)
},
beforeDestroy: function () {
+ // FIXME: If we call an action from here, there's no guarantee it will finish
+ // before the component is destroyed, which could bring up some problems
+ // Since I can't see any way to await it (because lifecycle hooks must be
+ // synchronous), unfortunately, we have to copy/paste the logic
+ // from the `setRandomCurrentInvidiousInstance` action onto here
if (this.currentInvidiousInstance === '') {
- // FIXME: If we call an action from here, there's no guarantee it will finish
- // before the component is destroyed, which could bring up some problems
- // Since I can't see any way to await it (because lifecycle hooks must be
- // synchronous), unfortunately, we have to copy/paste the logic
- // from the `setRandomCurrentInvidiousInstance` action onto here
const instanceList = this.invidiousInstancesList
this.setCurrentInvidiousInstance(randomArrayItem(instanceList))
}
+
+ if (this.setCurrentPipedInstance === '') {
+ const instanceList = this.pipedInstanceList
+ const randomIndex = Math.floor(Math.random() * instanceList.length)
+ this.setCurrentPipedInstance(instanceList[randomIndex])
+ }
},
methods: {
handleInvidiousInstanceInput: function (input) {
@@ -231,7 +252,11 @@ export default defineComponent({
this.setCurrentInvidiousInstanceBounce(instance)
},
- handleSetDefaultInstanceClick: function () {
+ handlePipedInstanceInput: function (input) {
+ this.setCurrentPipedInstanceBounce(input)
+ },
+
+ handleSetDefaultInvidiousInstanceClick: function () {
const instance = this.currentInvidiousInstance
this.updateDefaultInvidiousInstance(instance)
@@ -239,13 +264,58 @@ export default defineComponent({
showToast(message)
},
- handleClearDefaultInstanceClick: function () {
+ handleSetDefaultPipedInstanceClick: function () {
+ const instance = this.currentPipedInstance
+ this.updateDefaultPipedInstance(instance)
+
+ const message = this.$t('Default Piped instance has been set to {instance}', { instance })
+ showToast(message)
+ },
+
+ handleClearDefaultInvidiousInstanceClick: function () {
this.updateDefaultInvidiousInstance('')
showToast(this.$t('Default Invidious instance has been cleared'))
},
+ handleClearDefaultPipedInstanceClick: function () {
+ this.updateDefaultPipedInstance('')
+ showToast(this.$t('Default Piped instance has been cleared'))
+ },
+
handlePreferredApiBackend: function (backend) {
this.updateBackendPreference(backend)
+ if (backend === 'piped') {
+ if (!this.backendFallback) {
+ this.updateBackendFallback(true)
+ }
+ }
+
+ if (this.fallbackPreference === backend) {
+ if (backend === 'invidious') {
+ if (process.env.SUPPORTS_LOCAL_API) {
+ this.updateFallbackPreference('local')
+ } else {
+ this.updateFallbackPreference('piped')
+ }
+ } else {
+ this.updateFallbackPreference('invidious')
+ }
+ }
+ },
+
+ handleFallbackApiBackend: function (backend) {
+ this.updateFallbackPreference(backend)
+ if (this.backendPreference === backend) {
+ if (backend === 'invidious') {
+ if (process.env.SUPPORTS_LOCAL_API) {
+ this.handlePreferredApiBackend('local')
+ } else {
+ this.updateFallbackPreference('piped')
+ }
+ } else {
+ this.handlePreferredApiBackend('invidious')
+ }
+ }
},
handleThumbnailPreferenceChange: function (value) {
@@ -254,7 +324,8 @@ export default defineComponent({
},
...mapMutations([
- 'setCurrentInvidiousInstance'
+ 'setCurrentInvidiousInstance',
+ 'setCurrentPipedInstance'
]),
...mapActions([
@@ -265,7 +336,9 @@ export default defineComponent({
'updateCheckForBlogPosts',
'updateBarColor',
'updateBackendPreference',
+ 'updateFallbackPreference',
'updateDefaultInvidiousInstance',
+ 'updateDefaultPipedInstance',
'updateLandingPage',
'updateRegion',
'updateListType',
diff --git a/src/renderer/components/general-settings/general-settings.vue b/src/renderer/components/general-settings/general-settings.vue
index a2e1d76003326..9f2449911247f 100644
--- a/src/renderer/components/general-settings/general-settings.vue
+++ b/src/renderer/components/general-settings/general-settings.vue
@@ -13,6 +13,7 @@
+
-
-
-
-
-
-
-
-
+
- {{ $t('Settings.General Settings.The currently set default instance is {instance}', { instance: defaultInvidiousInstance }) }}
-
-
-
- {{ $t('Settings.General Settings.No default instance has been set') }}
-
-
- {{ $t('Settings.General Settings.Current instance will be randomized on startup') }}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/renderer/components/player-settings/player-settings.js b/src/renderer/components/player-settings/player-settings.js
index a2afa7e5dbad9..c50edc0686b1f 100644
--- a/src/renderer/components/player-settings/player-settings.js
+++ b/src/renderer/components/player-settings/player-settings.js
@@ -66,7 +66,11 @@ export default defineComponent({
},
computed: {
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
backendFallback: function () {
diff --git a/src/renderer/components/playlist-info/playlist-info.js b/src/renderer/components/playlist-info/playlist-info.js
index 51348d3e9256e..9b432cee73c8b 100644
--- a/src/renderer/components/playlist-info/playlist-info.js
+++ b/src/renderer/components/playlist-info/playlist-info.js
@@ -1,6 +1,7 @@
import { defineComponent, nextTick } from 'vue'
import { mapActions } from 'vuex'
import FtShareButton from '../ft-share-button/ft-share-button.vue'
+
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtInput from '../ft-input/ft-input.vue'
@@ -73,7 +74,7 @@ export default defineComponent({
},
viewCount: {
type: Number,
- required: true,
+ default: null
},
lastUpdated: {
type: String,
@@ -151,6 +152,10 @@ export default defineComponent({
return this.$store.getters.getBackendPreference
},
+ fallbackPreference: function () {
+ return this.$store.getters.getFallbackPreference
+ },
+
hideViews: function () {
return this.$store.getters.getHideVideoViews
},
@@ -205,24 +210,44 @@ export default defineComponent({
return thumbnailPlaceholder
}
- let baseUrl = 'https://i.ytimg.com'
- if (this.backendPreference === 'invidious') {
- baseUrl = this.currentInvidiousInstanceUrl
- } else if (typeof this.playlistThumbnail === 'string' && this.playlistThumbnail.length > 0) {
- // Use playlist thumbnail provided by YT when available
+ if (this.backendPreference === 'local' && typeof this.playlistThumbnail === 'string' && this.playlistThumbnail.length > 0) {
return this.playlistThumbnail
}
+ let baseUrl = ''
+ let backendPreference = this.backendPreference
+
+ if (backendPreference === 'piped') {
+ if (this.infoSource === 'piped' || this.playlistThumbnail) {
+ return this.playlistThumbnail
+ } else {
+ backendPreference = this.fallbackPreference
+ }
+ }
+
+ if (!process.env.SUPPORTS_LOCAL_API || backendPreference === 'invidious') {
+ baseUrl = this.currentInvidiousInstanceUrl
+ } else {
+ baseUrl = 'https://i.ytimg.com'
+ }
+
+ let imageUrl = ''
+
switch (this.thumbnailPreference) {
case 'start':
- return `${baseUrl}/vi/${this.firstVideoId}/mq1.jpg`
+ imageUrl = `${baseUrl}/vi/${this.firstVideoId}/mq1.jpg`
+ break
case 'middle':
- return `${baseUrl}/vi/${this.firstVideoId}/mq2.jpg`
+ imageUrl = `${baseUrl}/vi/${this.firstVideoId}/mq2.jpg`
+ break
case 'end':
- return `${baseUrl}/vi/${this.firstVideoId}/mq3.jpg`
+ imageUrl = `${baseUrl}/vi/${this.firstVideoId}/mq3.jpg`
+ break
default:
- return `${baseUrl}/vi/${this.firstVideoId}/mqdefault.jpg`
+ imageUrl = `${baseUrl}/vi/${this.firstVideoId}/mqdefault.jpg`
}
+
+ return imageUrl
},
isUserPlaylist() {
diff --git a/src/renderer/components/playlist-info/playlist-info.vue b/src/renderer/components/playlist-info/playlist-info.vue
index 3b594f1606ee8..b0aa1ce6cfdba 100644
--- a/src/renderer/components/playlist-info/playlist-info.vue
+++ b/src/renderer/components/playlist-info/playlist-info.vue
@@ -69,11 +69,11 @@
{{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }}
-
+
- {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
- -
-
+ -
+
{{ $t("Playlist.Last Updated On") }}
{{ lastUpdated }}
@@ -91,6 +91,12 @@
@input="(input) => newDescription = input"
@keydown.enter.native="savePlaylistInfo"
/>
+
+
{
this.searchSuggestionsDataList = results
+ }).catch((err) => {
+ console.error(err)
+ if (this.backendFallback && this.backendPreference === 'local') {
+ if (this.fallbackPreference === 'invidious') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Invidious API'
+ )
+ this.getSearchSuggestionsInvidious()
+ } else if (this.fallbackPreference === 'piped') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Piped API'
+ )
+ this.getSearchSuggestionsPiped(query)
+ }
+ }
})
},
@@ -330,11 +353,44 @@ export default defineComponent({
this.searchSuggestionsDataList = results.suggestions
}).catch((err) => {
console.error(err)
- if (process.env.SUPPORTS_LOCAL_API && this.backendFallback) {
- console.error(
- 'Error gettings search suggestions. Falling back to Local API'
- )
- this.getSearchSuggestionsLocal(query)
+ if (this.backendFallback && this.backendPreference === 'invidious') {
+ if (this.fallbackPreference === 'piped') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Piped API'
+ )
+ this.getSearchSuggestionsPiped(query)
+ } else if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Local API'
+ )
+ this.getSearchSuggestionsLocal(query)
+ }
+ }
+ })
+ },
+
+ getSearchSuggestionsPiped: function (query) {
+ if (query === '') {
+ this.searchSuggestionsDataList = []
+ return
+ }
+
+ getPipedSearchSuggestions(query).then((results) => {
+ this.searchSuggestionsDataList = results
+ }).catch((err) => {
+ console.error(err)
+ if (this.backendFallback && this.backendPreference === 'piped') {
+ if (this.fallbackPreference === 'invidious') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Invidious API'
+ )
+ this.getSearchSuggestionsInvidious(query)
+ } else if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ console.error(
+ 'Error gettings search suggestions. Falling back to Local API'
+ )
+ this.getSearchSuggestionsLocal(query)
+ }
}
})
},
diff --git a/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js
index 973eaddc551a9..fa71fc8d68b2a 100644
--- a/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js
+++ b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js
@@ -61,11 +61,15 @@ export default defineComponent({
},
computed: {
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
backendFallback: function () {
- return this.$store.getters.getBackendFallback
+ return this.$store.getters.getBackendFallback && this.$store.getters.getBackendPreference !== 'piped'
},
chatHeight: function () {
diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.js b/src/renderer/components/watch-video-playlist/watch-video-playlist.js
index e0d424bad8b62..7478f1783bc10 100644
--- a/src/renderer/components/watch-video-playlist/watch-video-playlist.js
+++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.js
@@ -10,6 +10,7 @@ import {
untilEndOfLocalPlayList,
} from '../../helpers/api/local'
import { invidiousGetPlaylistInfo } from '../../helpers/api/invidious'
+import { getPipedPlaylist, getPipedPlaylistMore } from '../../helpers/api/piped'
import { getSortedPlaylistItems, SORT_BY_VALUES } from '../../helpers/playlists'
export default defineComponent({
@@ -64,6 +65,10 @@ export default defineComponent({
return this.$store.getters.getBackendPreference
},
+ fallbackPreference: function () {
+ return this.$store.getters.getFallbackPreference
+ },
+
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
@@ -197,7 +202,9 @@ export default defineComponent({
},
playlistId: function (newVal, oldVal) {
if (oldVal !== newVal) {
- if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
+ if (this.backendPreference === 'piped') {
+ this.getPlaylistInformationPiped()
+ } else if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
this.getPlaylistInformationInvidious()
} else {
this.getPlaylistInformationLocal()
@@ -253,6 +260,8 @@ export default defineComponent({
if (this.selectedUserPlaylist != null) {
this.parseUserPlaylist(this.selectedUserPlaylist)
+ } else if (this.backendPreference === 'piped') {
+ this.getPlaylistInformationPiped()
} else if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
this.getPlaylistInformationInvidious()
} else {
@@ -405,7 +414,26 @@ export default defineComponent({
this.channelName = cachedPlaylist.channelName
this.channelId = cachedPlaylist.channelId
- if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious' || cachedPlaylist.continuationData === null) {
+ if (this.backendPreference === 'piped') {
+ const items = cachedPlaylist.items
+ let nextpage = cachedPlaylist.continuationData
+ while (nextpage != null) {
+ const moreInfo = await getPipedPlaylistMore({
+ playlistId: cachedPlaylist.id,
+ continuation: nextpage
+ })
+
+ items.push(...moreInfo.videos)
+
+ if (!moreInfo.nextpage) {
+ nextpage = null
+ } else {
+ nextpage = moreInfo.nextpage
+ }
+ }
+
+ this.playlistItems = items
+ } else if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious' || cachedPlaylist.continuationData === null) {
this.playlistItems = cachedPlaylist.items
} else {
const videos = cachedPlaylist.items
@@ -455,8 +483,54 @@ export default defineComponent({
copyToClipboard(err)
})
if (this.backendPreference === 'local' && this.backendFallback) {
- showToast(this.$t('Falling back to Invidious API'))
- this.getPlaylistInformationInvidious()
+ if (this.fallbackPreference === 'invidious') {
+ showToast(this.$t('Falling back to Invidious API'))
+ this.getPlaylistInformationInvidious()
+ } else {
+ showToast(this.$t('Falling back to Piped API'))
+ this.getPlaylistInformationPiped()
+ }
+ } else {
+ this.isLoading = false
+ }
+ }
+ },
+
+ getPlaylistInformationPiped: async function() {
+ this.isLoading = true
+ try {
+ const playlistInfo = await getPipedPlaylist(this.playlistId)
+ this.playlistTitle = playlistInfo.playlist.title
+ this.channelName = playlistInfo.playlist.channelName
+ this.channelId = playlistInfo.playlist.channelId
+ let nextpage = playlistInfo.nextpage
+ const videos = playlistInfo.videos
+ while (nextpage != null) {
+ const playlistContInfo = await getPipedPlaylistMore({
+ playlistId: this.playlistId,
+ continuation: nextpage
+ })
+ nextpage = playlistContInfo.nextpage
+ videos.push(...playlistContInfo.videos)
+ }
+ this.playlistItems = videos
+ this.isLoading = false
+ } catch (err) {
+ console.error(err)
+ const errorMessage = this.$t('Piped API Error (Click to copy)')
+ showToast(`${errorMessage}: ${err}`, 10000, () => {
+ copyToClipboard(err)
+ })
+ if (this.backendPreference === 'piped' && this.backendFallback) {
+ if (this.fallbackPreference === 'invidious') {
+ showToast(this.$t('Falling back to Invidious API'))
+ this.getPlaylistInformationInvidious()
+ } else if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ showToast(this.$t('Falling back to Local API'))
+ this.getPlaylistInformationLocal()
+ } else {
+ this.isLoading = false
+ }
} else {
this.isLoading = false
}
@@ -480,9 +554,16 @@ export default defineComponent({
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.getPlaylistInformationLocal()
+ if (this.backendPreference === 'invidious' && this.backendFallback) {
+ if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ showToast(this.$t('Falling back to Local API'))
+ this.getPlaylistInformationLocal()
+ } else if (this.fallbackPreference === 'piped') {
+ showToast(this.$t('Falling back to Piped API'))
+ this.getPlaylistInformationPiped()
+ } else {
+ this.isLoading = false
+ }
} else {
this.isLoading = false
// TODO: Show toast with error message
diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
index 85abbd3efb880..4dd4ab1b174b5 100644
--- a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
+++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
@@ -125,7 +125,7 @@
>
| Promise<{isError : boolean, error: string}>}
+ */
+export async function pipedRequest({ resource, id = '', params = {}, doLogError = true, subResource = '' }) {
+ const requestUrl = getCurrentInstance() + '/' + resource + '/' + id + (!isNullOrEmpty(subResource) ? `/${subResource}` : '') + '?' + new URLSearchParams(params).toString()
+ return await fetch(requestUrl)
+ .then((response) => response.json())
+ .then((json) => {
+ if (json.error !== undefined) {
+ throw new Error(json.error)
+ }
+ return json
+ })
+ .catch((error) => {
+ if (doLogError) {
+ console.error('Piped API error', requestUrl, error)
+ }
+ return { isError: true, error }
+ })
+}
+
+/**
+ * @typedef {{url: string, type: string, title: string, thumbnail: string, uploaderName: string, uploaderUrl: string, uploaderAvatar: string, uploadedDate: string, shortDescription: string, duration: number, views: number, uploaded: number, uploaderVerified: boolean, isShort: boolean}} PipedVideoType
+ * @typedef {{author: string, thumbnail: string, commentId: string, commentText: string, commentedTime: string, commentorUrl : string, repliesPage: string, likeCount: number, replyCount: number, hearted: boolean, pinned: boolean, verified: boolean, creatorReplied: boolean, channelOwner: boolean}} PipedCommentsType
+ * @typedef {{name: string, thumbnailUrl: string, description: string, bannerUrl: string, nextpage: string, uploader: string, uploaderUrl: string, uploaderAvatar: string, videos: number, relatedStreams: PipedVideoType[]}} PipedPlaylistType
+ * @typedef {{isError: true, error: string}} PipedError
+ */
+
+/**
+ * @param {string} videoId
+ */
+export async function getPipedComments(videoId) {
+ /** @type { PipedError | {nextpage: string?, commentCount: number?, disabled: boolean?, comments: PipedCommentsType[]?} } */
+ const commentInfo = await pipedRequest({ resource: 'comments', id: videoId })
+ if (commentInfo.isError) {
+ throw commentInfo.error
+ } else {
+ commentInfo.comments = parsePipedComments(commentInfo.comments)
+ return {
+ comments: commentInfo.comments,
+ continuation: commentInfo.nextpage
+ }
+ }
+}
+
+/**
+ * @param {object} params - The parameters for fetching comments.
+ * @param {string} params.videoId - The ID of the video for which to fetch comments.
+ * @param {string} params.continuation - The continuation token for paginated comments.
+ */
+export async function getPipedCommentsMore({ videoId, continuation }) {
+ /** @type {PipedError | {nextpage: string, commentCount: number, disabled: boolean, comments: PipedCommentsType[] }} */
+ const commentInfo = await pipedRequest({
+ resource: 'nextpage/comments',
+ id: videoId,
+ params: {
+ nextpage: continuation
+ }
+ })
+
+ if (commentInfo.isError) {
+ throw commentInfo.error
+ } else {
+ commentInfo.comments = parsePipedComments(commentInfo.comments)
+ return {
+ comments: commentInfo.comments,
+ continuation: commentInfo.nextpage
+ }
+ }
+}
+/**
+ * @param {PipedCommentsType[]} comments
+ */
+function parsePipedComments(comments) {
+ return comments.map(comment => {
+ const authorId = comment.commentorUrl.replace('/channel/', '')
+ return {
+ dataType: 'piped',
+ author: comment.author,
+ authorId: authorId,
+ authorLink: authorId,
+ authorThumb: comment.thumbnail,
+ commentId: comment.commentId,
+ id: comment.commentId,
+ text: comment.commentText,
+ isPinned: comment.pinned,
+ isVerified: comment.verified,
+ numReplies: comment.replyCount,
+ likes: comment.likeCount,
+ isHearted: comment.hearted,
+ replyToken: comment.repliesPage,
+ hasReplyToken: !!comment.repliesPage,
+ isMember: false,
+ isOwner: comment.channelOwner,
+ showReplies: false,
+ replies: [],
+ hasOwnerReplied: comment.creatorReplied,
+ time: getRelativeTimeFromDate(calculatePublishedDate(comment.commentedTime), false),
+ }
+ })
+}
+
+/**
+ * @param {string} playlistId
+ */
+export async function getPipedPlaylist(playlistId) {
+ /** @type {{PipedError | PipedPlaylistType}} */
+ const playlistInfo = await pipedRequest({ resource: 'playlists', id: playlistId })
+ if (playlistInfo.isError) {
+ throw playlistInfo.error
+ } else {
+ const parsedVideos = parsePipedVideos(playlistInfo.relatedStreams)
+ return {
+ nextpage: playlistInfo.nextpage,
+ playlist: parsePipedPlaylist(playlistId, playlistInfo, parsedVideos),
+ videos: parsedVideos
+ }
+ }
+}
+
+/**
+ * @param {object} params - The parameters for fetching comments.
+ * @param {string} params.playlistId - The ID of the video for which to fetch comments.
+ * @param {string} params.continuation - The continuation token for paginated comments.
+ */
+export async function getPipedPlaylistMore({ playlistId, continuation }) {
+ /** @type {PipedError | {nextpage: string, relatedStreams: PipedVideoType[]}} */
+ const playlistInfo = await pipedRequest({
+ resource: 'nextpage/playlists',
+ id: playlistId,
+ params: {
+ nextpage: continuation
+ }
+ })
+
+ if (playlistInfo.isError) {
+ throw playlistInfo.error
+ } else {
+ return {
+ nextpage: playlistInfo.nextpage,
+ videos: parsePipedVideos(playlistInfo.relatedStreams)
+ }
+ }
+}
+
+/**
+ * @param {string} query
+ * @returns {[string, string[]]}
+ */
+export async function getPipedSearchSuggestions(query) {
+ const searchInfo = await pipedRequest({
+ resource: 'opensearch/suggestions',
+ params: {
+ query
+ }
+ })
+
+ return searchInfo[1]
+}
+
+/**
+ * @param {string} url
+ * @returns {{host: string, imageProtocol : string, resource: string, baseUrl: string}?}
+ */
+export function getPipedUrlInfo(url) {
+ const regex = /^(?(https?:\/\/)[^/]*)\/((?vi|ytc)\/)?(?[^?]*).*host=(?[^&]*)/
+ return url.match(regex)?.groups
+}
+
+/**
+ * @param {string} url
+ * @returns {string}
+ */
+export function pipedImageToYouTube(url) {
+ const urlInfo = getPipedUrlInfo(url)
+ let newUrl = `https://${urlInfo.host}/`
+ if (!isNullOrEmpty(urlInfo.imageProtocol)) {
+ newUrl += `${urlInfo.imageProtocol}/`
+ }
+ newUrl += urlInfo.resource
+ return newUrl
+}
+
+/**
+ * @param {string} playlistId
+ * @param {PipedPlaylistType} result
+ * @param {ReturnType} parsedVideos
+ */
+function parsePipedPlaylist(playlistId, result, parsedVideos) {
+ return {
+ id: playlistId,
+ title: result.name,
+ description: result.description,
+ firstVideoId: parsedVideos[0].videoId,
+ viewCount: null,
+ videoCount: result.videos,
+ channelName: result.uploader,
+ channelThumbnail: result.uploaderAvatar,
+ channelId: result.uploaderUrl.replace('/channel/', ''),
+ firstVideoThumbnail: parsedVideos[0].thumbnail,
+ thumbnailUrl: result.thumbnailUrl,
+ infoSource: 'piped'
+ }
+}
+
+/**
+ * @param {PipedVideoType[]} videoList
+ */
+function parsePipedVideos(videoList) {
+ return videoList.map(video => {
+ return {
+ videoId: video.url.replace('/watch?v=', ''),
+ title: video.title,
+ author: video.uploaderName,
+ authorId: video.uploaderUrl.replace('/channel/', ''),
+ lengthSeconds: video.duration,
+ description: video.shortDescription,
+ publishedText: video.uploadedDate, // uploaded time stamp
+ viewCount: video.views,
+ thumbnail: video.thumbnail,
+ isVerified: video.uploaderVerified,
+ type: 'video'
+ }
+ })
+}
+
+/**
+ * @param {string} region
+ */
+export async function getPipedTrending(region) {
+ /** @type {{isError: boolean, error: string} | {PipedVideoType}[]} */
+ const trendingInfo = await pipedRequest({ resource: 'trending', params: { region } })
+ if (trendingInfo.isError) {
+ throw trendingInfo.error
+ } else {
+ return parsePipedVideos(trendingInfo)
+ }
+}
diff --git a/src/renderer/store/modules/index.js b/src/renderer/store/modules/index.js
index 144b0355c696f..040e05367b4c8 100644
--- a/src/renderer/store/modules/index.js
+++ b/src/renderer/store/modules/index.js
@@ -5,6 +5,7 @@
import history from './history'
import invidious from './invidious'
+import piped from './piped'
import playlists from './playlists'
import profiles from './profiles'
import settings from './settings'
@@ -15,6 +16,7 @@ import player from './player'
export default {
history,
invidious,
+ piped,
playlists,
profiles,
settings,
diff --git a/src/renderer/store/modules/piped.js b/src/renderer/store/modules/piped.js
new file mode 100644
index 0000000000000..9d06e674786a5
--- /dev/null
+++ b/src/renderer/store/modules/piped.js
@@ -0,0 +1,69 @@
+import { createWebURL, fetchWithTimeout } from '../../helpers/utils'
+
+const state = {
+ currentPipedInstance: '',
+ pipedInstancesList: null
+}
+
+const getters = {
+ getCurrentPipedInstance(state) {
+ return state.currentPipedInstance
+ },
+
+ getPipedInstancesList(state) {
+ return state.pipedInstancesList
+ }
+}
+
+const actions = {
+ async fetchPipedInstancesFromFile({ commit }) {
+ const url = createWebURL('/static/piped-instances.json')
+ const instances = await (await fetch(url)).json()
+ commit('setPipedInstancesList', instances)
+ },
+
+ async fetchPipedInstances({ commit }) {
+ const requestUrl = 'https://piped-instances.kavin.rocks/'
+ let instances = []
+ try {
+ const response = await fetchWithTimeout(15_000, requestUrl)
+ const json = await response.json()
+ instances = json.map(instance => instance.api_url)
+
+ if (instances.length !== 0) {
+ commit('setPipedInstancesList', instances)
+ } else {
+ console.warn('using static file for piped instances')
+ }
+ } catch (err) {
+ if (err.name === 'TimeoutError') {
+ console.error('Fetching the Piped instance list timed out after 15 seconds. Falling back to local copy.')
+ } else {
+ console.error(err)
+ }
+ }
+ },
+
+ setRandomCurrentPipedInstance({ commit, state }) {
+ const instanceList = state.pipedInstancesList
+ const randomIndex = Math.floor(Math.random() * instanceList.length)
+ commit('setCurrentPipedInstance', instanceList[randomIndex])
+ }
+}
+
+const mutations = {
+ setCurrentPipedInstance(state, value) {
+ state.currentPipedInstance = value
+ },
+
+ setPipedInstancesList(state, value) {
+ state.pipedInstancesList = value
+ }
+}
+
+export default {
+ state,
+ getters,
+ actions,
+ mutations
+}
diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js
index eb241922fd512..e8161e07787fd 100644
--- a/src/renderer/store/modules/settings.js
+++ b/src/renderer/store/modules/settings.js
@@ -166,6 +166,7 @@ const state = {
autoplayVideos: true,
backendFallback: process.env.SUPPORTS_LOCAL_API,
backendPreference: !process.env.SUPPORTS_LOCAL_API ? 'invidious' : 'local',
+ fallbackPreference: !process.env.SUPPORTS_LOCAL_API ? 'piped' : 'invidious',
barColor: false,
checkForBlogPosts: true,
checkForUpdates: true,
@@ -397,6 +398,15 @@ const stateWithSideEffects = {
}
},
+ defaultPipedInstance: {
+ defaultValue: '', // s
+ sideEffectsHandler: ({ commit, getters }, value) => {
+ if (value !== '' && getters.getCurrentInvidiousInstance !== value) {
+ commit('setCurrentPipedInstance', value)
+ }
+ }
+ },
+
defaultVolume: {
defaultValue: 1,
sideEffectsHandler: (_, value) => {
diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js
index ccdb36d3827c4..f77a2445cf07b 100644
--- a/src/renderer/views/Channel/Channel.js
+++ b/src/renderer/views/Channel/Channel.js
@@ -152,11 +152,15 @@ export default defineComponent({
},
computed: {
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
backendFallback: function () {
- return this.$store.getters.getBackendFallback
+ return this.$store.getters.getBackendFallback && this.$store.getters.getBackendPreference !== 'piped'
},
showFamilyFriendlyOnly: function() {
diff --git a/src/renderer/views/Hashtag/Hashtag.vue b/src/renderer/views/Hashtag/Hashtag.vue
index 7de7ada063cf8..3a5b627cfe150 100644
--- a/src/renderer/views/Hashtag/Hashtag.vue
+++ b/src/renderer/views/Hashtag/Hashtag.vue
@@ -71,12 +71,16 @@ const isLoading = ref(true)
/** @type {import('vue').ComputedRef<'local' | 'invidious'>} */
const backendPreference = computed(() => {
- return store.getters.getBackendPreference
+ let preference = store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = store.getters.getFallbackPreference
+ }
+ return preference
})
/** @type {import('vue').ComputedRef} */
const backendFallback = computed(() => {
- return store.getters.getBackendFallback
+ return store.getters.getBackendFallback && store.getters.getBackendPreference !== 'piped'
})
const showFetchMoreButton = computed(() => {
diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js
index 91ddd0c3e8843..856cd39ee8c30 100644
--- a/src/renderer/views/Playlist/Playlist.js
+++ b/src/renderer/views/Playlist/Playlist.js
@@ -22,6 +22,7 @@ import {
deepCopy,
} from '../../helpers/utils'
import { invidiousGetPlaylistInfo, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
+import { getPipedPlaylist, getPipedPlaylistMore, pipedImageToYouTube } from '../../helpers/api/piped'
import { getSortedPlaylistItems, videoDurationPresent, videoDurationWithFallback, SORT_BY_VALUES } from '../../helpers/playlists'
import packageDetails from '../../../../package.json'
import { MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD } from '../../../constants'
@@ -86,6 +87,9 @@ export default defineComponent({
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
+ fallbackPreference: function () {
+ return this.$store.getters.getFallbackPreference
+ },
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
@@ -303,6 +307,9 @@ export default defineComponent({
case 'invidious':
this.getPlaylistInvidious()
break
+ case 'piped':
+ this.getPlaylistPiped()
+ break
}
},
getPlaylistLocal: function () {
@@ -358,8 +365,13 @@ export default defineComponent({
}).catch((err) => {
console.error(err)
if (this.backendPreference === 'local' && this.backendFallback) {
- console.warn('Falling back to Invidious API')
- this.getPlaylistInvidious()
+ if (this.fallbackPreference === 'invidious') {
+ console.warn('Falling back to Invidious API')
+ this.getPlaylistInvidious()
+ } else {
+ console.warn('Falling back to Piped API')
+ this.getPlaylistPiped()
+ }
} else {
this.isLoading = false
}
@@ -394,9 +406,16 @@ export default defineComponent({
this.isLoading = false
}).catch((err) => {
console.error(err)
- if (process.env.SUPPORTS_LOCAL_API && this.backendPreference === 'invidious' && this.backendFallback) {
- console.warn('Error getting data with Invidious, falling back to local backend')
- this.getPlaylistLocal()
+ if (this.backendPreference === 'invidious' && this.backendFallback) {
+ if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ console.warn('Error getting data with Invidious, falling back to Local backend')
+ this.getPlaylistLocal()
+ } else if (this.fallbackPreference === 'piped') {
+ console.warn('Error getting data with Invidious, falling back to Piped backend')
+ this.getPlaylistPiped()
+ } else {
+ this.isLoading = false
+ }
} else {
this.isLoading = false
// TODO: Show toast with error message
@@ -404,6 +423,48 @@ export default defineComponent({
})
},
+ getPlaylistPiped: async function () {
+ try {
+ this.isLoading = true
+ const { playlist, videos, nextpage } = await getPipedPlaylist(this.playlistId)
+ this.playlistTitle = playlist.title
+ this.playlistDescription = playlist.description
+ this.firstVideoId = videos.at(0)?.videoId
+ this.playlistThumbnail = playlist.thumbnailUrl
+ this.viewCount = playlist.viewCount
+ this.videoCount = playlist.videoCount
+ this.channelName = playlist.channelName
+ this.channelThumbnail = playlist.channelThumbnail
+ this.channelId = playlist.channelId
+ this.infoSource = 'piped'
+ this.continuationData = nextpage
+ this.playlistItems = videos
+
+ this.updateSubscriptionDetails({
+ channelThumbnailUrl: pipedImageToYouTube(playlist.channelThumbnail),
+ channelName: playlist.channelName,
+ channelId: playlist.channelId
+ })
+ this.isLoading = false
+ } catch (err) {
+ console.error(err)
+ if (this.backendPreference === 'piped' && this.backendFallback) {
+ if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ console.warn('Error getting data with Piped, falling back to Local backend')
+ this.getPlaylistLocal()
+ } else if (this.fallbackPreference === 'invidious') {
+ console.warn('Error getting data with Piped, falling back to Invidious backend')
+ this.getPlaylistInvidious()
+ } else {
+ this.isLoading = false
+ }
+ } else {
+ this.isLoading = false
+ // TODO: Show toast with error message
+ }
+ }
+ },
+
parseUserPlaylist: function (playlist) {
this.playlistTitle = playlist.playlistName
this.playlistDescription = playlist.description ?? ''
@@ -486,6 +547,9 @@ export default defineComponent({
case 'invidious':
console.error('Playlist pagination is not currently supported when the Invidious backend is selected.')
break
+ case 'piped':
+ this.getNextPagePiped()
+ break
}
},
@@ -516,6 +580,22 @@ export default defineComponent({
})
},
+ getNextPagePiped: async function() {
+ this.isLoadingMore = true
+ const { videos, nextpage } = await getPipedPlaylistMore({
+ playlistId: this.playlistId,
+ continuation: this.continuationData
+ })
+
+ this.playlistItems = this.playlistItems.concat(videos)
+ if (nextpage) {
+ this.continuationData = nextpage
+ } else {
+ this.continuationData = null
+ }
+ this.isLoadingMore = false
+ },
+
moveVideoUp: function (videoId, playlistItemId) {
const playlistItems = [].concat(this.playlistItems)
const videoIndex = playlistItems.findIndex((video) => {
diff --git a/src/renderer/views/Post.vue b/src/renderer/views/Post.vue
index 2320dec647bc5..7acfd61179f3b 100644
--- a/src/renderer/views/Post.vue
+++ b/src/renderer/views/Post.vue
@@ -50,7 +50,7 @@ const authorId = ref('')
const post = shallowRef(null)
const isLoading = ref(true)
-/** @type {import('vue').ComputedRef<'invidious' | 'local'>} */
+/** @type {import('vue').ComputedRef<'invidious' | 'local' | 'piped'>} */
const backendPreference = computed(() => {
return store.getters.getBackendPreference
})
@@ -60,8 +60,13 @@ const backendFallback = computed(() => {
return store.getters.getBackendFallback
})
+/** @type {import('vue').ComputedRef<'invidious' | 'local' | 'piped'>} */
+const fallbackPreference = computed(() => {
+ return store.getters.getFallbackPreference
+})
+
const isInvidiousAllowed = computed(() => {
- return backendPreference.value === 'invidious' || backendFallback.value
+ return backendPreference.value === 'invidious' || (backendFallback.value && fallbackPreference.value === 'invidious')
})
onMounted(async () => {
diff --git a/src/renderer/views/Search/Search.js b/src/renderer/views/Search/Search.js
index eb274ed5ad22e..69198054fa848 100644
--- a/src/renderer/views/Search/Search.js
+++ b/src/renderer/views/Search/Search.js
@@ -40,11 +40,15 @@ export default defineComponent({
},
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
backendFallback: function () {
- return this.$store.getters.getBackendFallback
+ return this.$store.getters.getBackendFallback && this.$store.getters.getBackendPreference !== 'piped'
},
showFamilyFriendlyOnly: function() {
diff --git a/src/renderer/views/SubscribedChannels/SubscribedChannels.vue b/src/renderer/views/SubscribedChannels/SubscribedChannels.vue
index 742eb469e86ab..a6aeb27161fc4 100644
--- a/src/renderer/views/SubscribedChannels/SubscribedChannels.vue
+++ b/src/renderer/views/SubscribedChannels/SubscribedChannels.vue
@@ -139,7 +139,11 @@ const hideUnsubscribeButton = computed(() => {
/** @type {import('vue').ComputedRef<'local' | 'invidious'>} */
const backendPreference = computed(() => {
- return store.getters.getBackendPreference
+ let preference = store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = store.getters.getFallbackPreference
+ }
+ return preference
})
/** @type {import('vue').ComputedRef} */
diff --git a/src/renderer/views/Trending/Trending.js b/src/renderer/views/Trending/Trending.js
index 915970dc7cb34..a8380ee37eb70 100644
--- a/src/renderer/views/Trending/Trending.js
+++ b/src/renderer/views/Trending/Trending.js
@@ -10,6 +10,7 @@ import FtRefreshWidget from '../../components/ft-refresh-widget/ft-refresh-widge
import { copyToClipboard, getRelativeTimeFromDate, showToast } from '../../helpers/utils'
import { getLocalTrending } from '../../helpers/api/local'
import { getInvidiousTrending } from '../../helpers/api/invidious'
+import { getPipedTrending } from '../../helpers/api/piped'
import { KeyboardShortcuts } from '../../../constants'
export default defineComponent({
@@ -27,13 +28,17 @@ export default defineComponent({
isLoading: false,
shownResults: [],
currentTab: 'default',
- trendingInstance: null
+ trendingInstance: null,
+ hideTabs: false
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
+ fallbackPreference: function() {
+ return this.$store.getters.getFallbackPreference
+ },
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
@@ -50,6 +55,8 @@ export default defineComponent({
mounted: function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
+ this.hideTabs = this.backendPreference === 'piped' && !this.backendFallback
+
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
this.getTrendingInfoCache()
} else {
@@ -91,7 +98,9 @@ export default defineComponent({
this.$store.commit('clearTrendingCache')
}
- if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
+ if (this.backendPreference === 'piped') {
+ this.getTrendingInfoPiped()
+ } else if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') {
this.getTrendingInfoInvidious()
} else {
this.getTrendingInfoLocal()
@@ -120,9 +129,14 @@ export default defineComponent({
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
- if (this.backendPreference === 'local' && this.backendFallback) {
- showToast(this.$t('Falling back to Invidious API'))
- this.getTrendingInfoInvidious()
+ if (this.backendFallback && this.backendPreference === 'local') {
+ if (this.fallbackPreference === 'invidious') {
+ showToast(this.$t('Falling back to Invidious API'))
+ this.getTrendingInfoInvidious()
+ } else if (this.fallbackPreference === 'piped') {
+ showToast(this.$t('Falling back to Piped API'))
+ this.getTrendingInfoPiped()
+ }
} else {
this.isLoading = false
}
@@ -153,15 +167,58 @@ export default defineComponent({
copyToClipboard(err)
})
- if (process.env.SUPPORTS_LOCAL_API && (this.backendPreference === 'invidious' && this.backendFallback)) {
- showToast(this.$t('Falling back to Local API'))
- this.getTrendingInfoLocal()
+ if (this.backendFallback && this.backendPreference === 'invidious') {
+ if (this.fallbackPreference === 'piped') {
+ showToast(this.$t('Falling back to Piped API'))
+ this.getTrendingInfoPiped()
+ } else if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ showToast(this.$t('Falling back to Local API'))
+ this.getTrendingInfoLocal()
+ }
} else {
this.isLoading = false
}
})
},
+ getTrendingInfoPiped: async function() {
+ try {
+ this.hideTabs = this.backendPreference === 'piped' && !this.backendFallback
+ this.isLoading = true
+ if (this.currentTab === 'default') {
+ this.currentTab = 'default'
+ const returnData = await getPipedTrending(this.region)
+ this.shownResults = returnData
+ this.isLoading = false
+ this.$store.commit('setTrendingCache', { value: returnData, page: this.currentTab })
+ } else {
+ throw new Error('Trending Tabs are not supported by Piped')
+ }
+ } catch (err) {
+ if (err.message !== 'Trending Tabs are not supported by Piped' || this.backendPreference !== 'piped') {
+ console.error(err)
+ const errorMessage = this.$t('Piped API Error (Click to copy)')
+ showToast(`${errorMessage}: ${err}`, 10000, () => {
+ copyToClipboard(err)
+ })
+ } else {
+ console.error(err.message)
+ }
+
+ if (this.backendFallback && this.backendPreference === 'piped') {
+ if (this.fallbackPreference === 'invidious') {
+ showToast(this.$t('Falling back to Invidious API'))
+ this.getTrendingInfoInvidious()
+ } else if (process.env.SUPPORTS_LOCAL_API && this.fallbackPreference === 'local') {
+ showToast(this.$t('Falling back to Local API'))
+ this.getTrendingInfoLocal()
+ }
+ } else {
+ this.isLoading = false
+ }
+ }
+ },
+
/**
* This function `keyboardShortcutHandler` should always be at the bottom of this file
* @param {KeyboardEvent} event the keyboard event
diff --git a/src/renderer/views/Trending/Trending.vue b/src/renderer/views/Trending/Trending.vue
index 2a5f2f15ec20b..4f22ae5594512 100644
--- a/src/renderer/views/Trending/Trending.vue
+++ b/src/renderer/views/Trending/Trending.vue
@@ -17,6 +17,7 @@
{{ $t("Trending.Trending") }}
diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js
index 040c616ddefe1..19df68d5697a0 100644
--- a/src/renderer/views/Watch/Watch.js
+++ b/src/renderer/views/Watch/Watch.js
@@ -153,10 +153,14 @@ export default defineComponent({
return this.$store.getters.getSaveVideoHistoryWithLastViewedPlaylist
},
backendPreference: function () {
- return this.$store.getters.getBackendPreference
+ let preference = this.$store.getters.getBackendPreference
+ if (preference === 'piped') {
+ preference = this.$store.getters.getFallbackPreference
+ }
+ return preference
},
backendFallback: function () {
- return this.$store.getters.getBackendFallback
+ return this.$store.getters.getBackendFallback && this.$store.getters.getBackendPreference !== 'piped'
},
currentInvidiousInstanceUrl: function () {
return this.$store.getters.getCurrentInvidiousInstanceUrl
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index f27e20676b2b0..e4fe482d4beab 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -293,8 +293,10 @@ Settings:
System Default: System Default
Preferred API Backend:
Preferred API Backend: Preferred API Backend
+ Fallback API Backend: Fallback API Backend
Local API: Local API
Invidious API: Invidious API
+ Piped API: Piped API
Video View Type:
Video View Type: Video View Type
Grid: Grid
@@ -308,12 +310,14 @@ Settings:
Hidden: Hidden
Blur: Blur
Current Invidious Instance: Current Invidious Instance
+ Current Piped Instance: Current Piped Instance
The currently set default instance is {instance}: The currently set default instance is {instance}
No default instance has been set: No default instance has been set
Current instance will be randomized on startup: Current instance will be randomized on startup
Set Current Instance as Default: Set Current Instance as Default
Clear Default Instance: Clear Default Instance
View all Invidious instance information: View all Invidious instance information
+ View all Piped instance information: View all Piped instance information
Region for Trending: Region for Trending
#! List countries
External Link Handling:
@@ -1006,16 +1010,19 @@ Description:
#Tooltips
Tooltips:
General Settings:
- Preferred API Backend: Choose the backend that FreeTube uses to obtain data. The
+ Preferred API Backend: "Choose the backend that FreeTube uses to obtain data. The
local API is a built-in extractor. The Invidious API requires an Invidious server
- to connect to.
+ to connect to. The Piped API requires a Piped server to connect to."
Fallback to Non-Preferred Backend on Failure: When your preferred API has a problem,
FreeTube will automatically attempt to use your non-preferred API as a fallback
method when enabled.
+ Fallback API Backend: Choose the fallback backend that FreeTube uses when there is a problem with the preferred API.
Thumbnail Preference: All thumbnails throughout FreeTube will be replaced with
a frame of the video instead of the default thumbnail.
Invidious Instance: The Invidious instance that FreeTube will connect to for API
calls.
+ Piped Instance: The Piped instance that FreeTube will connect to for API
+ calls.
Region for Trending: The region of trends allows you to pick which country's trending
videos you want to have displayed.
External Link Handling: |
@@ -1067,7 +1074,9 @@ Tooltips:
# Toast Messages
Local API Error (Click to copy): Local API Error (Click to copy)
Invidious API Error (Click to copy): Invidious API Error (Click to copy)
+Piped API Error (Click to copy): Piped API Error (Click to copy)
Falling back to Invidious API: Falling back to Invidious API
+Falling back to Piped API: Falling back to Piped API
Falling back to Local API: Falling back to Local API
This video is unavailable because of missing formats. This can happen due to country unavailability.: This
video is unavailable because of missing formats. This can happen due to country
@@ -1084,6 +1093,8 @@ Playlist will not pause when current video is finished: Playlist will not pause
Playlist will pause when current video is finished: Playlist will pause when current video is finished
Playing Next Video Interval: Playing next video in no time. Click to cancel. | Playing next video in {nextVideoInterval} second. Click to cancel. | Playing next video in {nextVideoInterval} seconds. Click to cancel.
Canceled next video autoplay: Canceled next video autoplay
+Default Piped instance has been set to {instance}: Default Piped instance has been set to {instance}
+Default Piped instance has been cleared: Default Piped instance has been cleared
Autoplay Interruption Timer: Autoplay canceled due to {autoplayInterruptionIntervalHours} hours of inactivity
Default Invidious instance has been set to {instance}: Default Invidious instance has been set to {instance}
diff --git a/static/piped-instances.json b/static/piped-instances.json
new file mode 100644
index 0000000000000..932354f3ed565
--- /dev/null
+++ b/static/piped-instances.json
@@ -0,0 +1,12 @@
+[
+ "https://pipedapi.leptons.xyz",
+ "https://pipedapi.nosebs.ru",
+ "https://pipedapi.in.projectsegfau.lt",
+ "https://pipedapi.smnz.de",
+ "https://pipedapi.adminforge.de",
+ "https://api.piped.yt",
+ "https://pipedapi.drgns.space",
+ "https://pipedapi.ngn.tf",
+ "https://piped-api.codespace.cz",
+ "https://api.piped.private.coffee"
+]
\ No newline at end of file