2023-01-18 08:50:02 +01:00
|
|
|
import { defineComponent } from 'vue'
|
2022-09-24 11:12:11 +02:00
|
|
|
import FtSettingsSection from '../ft-settings-section/ft-settings-section.vue'
|
2020-09-07 00:12:25 +02:00
|
|
|
import { mapActions, mapMutations } from 'vuex'
|
|
|
|
import FtButton from '../ft-button/ft-button.vue'
|
|
|
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
|
|
|
import FtPrompt from '../ft-prompt/ft-prompt.vue'
|
2024-01-03 19:44:57 +01:00
|
|
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
Store Revamp / Full database synchronization across windows (#1833)
* History: Refactor history module
* Profiles: Refactor profiles module
* IPC: Move channel ids to their own file and make them constants
* IPC: Replace single sync channel for one channel per sync type
* Everywhere: Replace default profile id magic strings with constant ref
* Profiles: Refactor `activeProfile` property from store
This commit makes it so that `activeProfile`'s getter returns
the entire profile, while the related update function only needs
the profile id (instead of the previously used array index)
to change the currently active profile.
This change was made due to inconsistency regarding the active profile
when creating new profiles.
If a new profile coincidentally landed in the current active profile's
array index after sorting, the app would mistakenly change to it
without any action from the user apart from the profile's creation.
Turning the profile id into the selector instead solves this issue.
* Revert "Store: Implement history synchronization between windows"
This reverts commit 99b61e617873412eb393d8f4dfccd8f8c172021f.
This is necessary for an upcoming improved implementation of the
history synchronization.
* History: Remove unused mutation
* Everywhere: Create abstract database handlers
The project now utilizes abstract handlers to fetch, modify
or otherwise manipulate data from the database.
This facilitates 3 aspects of the app, in addition of
making them future proof:
- Switching database libraries is now trivial
Since most of the app utilizes the abstract handlers, it's incredibly
easily to change to a different DB library.
Hypothetically, all that would need to be done is to simply replace the
the file containing the base handlers, while the rest of the app
would go unchanged.
- Syncing logic between Electron and web is now properly separated
There are now two distinct DB handling APIs: the Electron one and
the web one.
The app doesn't need to manually choose the API, because it's detected
which platform is being utilized on import.
- All Electron windows now share the same database instance
This provides a single source of truth, improving consistency
regarding data manipulation and windows synchronization.
As a sidenote, syncing implementation has been left as is
(web unimplemented; Electron only syncs settings, remaining
datastore syncing will be implemented in the upcoming commits).
* Electron/History: Implement history synchronization
* Profiles: Implement suplementary profile creation logic
* ft-profile-edit: Small fix on profile name missing display
* Electron/Profiles: Implement profile synchronization
* Electron/Playlists: Implement playlist synchronization
2021-12-15 19:42:24 +01:00
|
|
|
import { MAIN_PROFILE_ID } from '../../../constants'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2022-11-09 06:57:48 +01:00
|
|
|
import { calculateColorLuminance, getRandomColor } from '../../helpers/colors'
|
2022-10-25 16:44:18 +02:00
|
|
|
import {
|
|
|
|
copyToClipboard,
|
2023-08-15 20:17:10 +02:00
|
|
|
deepCopy,
|
2023-06-19 19:01:12 +02:00
|
|
|
escapeHTML,
|
2023-03-02 22:11:36 +01:00
|
|
|
getTodayDateStrLocalTimezone,
|
2022-10-25 16:44:18 +02:00
|
|
|
readFileFromDialog,
|
|
|
|
showOpenDialog,
|
|
|
|
showSaveDialog,
|
|
|
|
showToast,
|
2023-03-02 22:11:36 +01:00
|
|
|
writeFileFromDialog,
|
2022-10-25 16:44:18 +02:00
|
|
|
} from '../../helpers/utils'
|
2023-01-12 07:55:21 +01:00
|
|
|
import { invidiousAPICall } from '../../helpers/api/invidious'
|
2023-03-01 01:39:33 +01:00
|
|
|
import { getLocalChannel } from '../../helpers/api/local'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2023-01-18 08:50:02 +01:00
|
|
|
export default defineComponent({
|
2020-09-07 00:12:25 +02:00
|
|
|
name: 'DataSettings',
|
|
|
|
components: {
|
2022-09-24 11:12:11 +02:00
|
|
|
'ft-settings-section': FtSettingsSection,
|
2020-09-07 00:12:25 +02:00
|
|
|
'ft-button': FtButton,
|
|
|
|
'ft-flex-box': FtFlexBox,
|
2024-01-03 19:44:57 +01:00
|
|
|
'ft-prompt': FtPrompt,
|
|
|
|
'ft-toggle-switch': FtToggleSwitch,
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
data: function () {
|
|
|
|
return {
|
|
|
|
showExportSubscriptionsPrompt: false,
|
|
|
|
subscriptionsPromptValues: [
|
|
|
|
'freetube',
|
2021-08-25 09:47:21 +02:00
|
|
|
'youtubenew',
|
2020-09-07 00:12:25 +02:00
|
|
|
'youtube',
|
2020-11-01 22:23:06 +01:00
|
|
|
'youtubeold',
|
2020-09-07 00:12:25 +02:00
|
|
|
'newpipe'
|
2024-01-03 19:44:57 +01:00
|
|
|
],
|
|
|
|
|
|
|
|
shouldExportPlaylistForOlderVersions: false,
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
backendPreference: function () {
|
|
|
|
return this.$store.getters.getBackendPreference
|
|
|
|
},
|
|
|
|
backendFallback: function () {
|
|
|
|
return this.$store.getters.getBackendFallback
|
|
|
|
},
|
|
|
|
profileList: function () {
|
|
|
|
return this.$store.getters.getProfileList
|
|
|
|
},
|
2022-02-06 20:31:27 +01:00
|
|
|
allPlaylists: function () {
|
|
|
|
return this.$store.getters.getAllPlaylists
|
|
|
|
},
|
2023-09-14 03:31:07 +02:00
|
|
|
historyCacheSorted: function () {
|
|
|
|
return this.$store.getters.getHistoryCacheSorted
|
2022-10-19 07:50:21 +02:00
|
|
|
},
|
2020-09-07 00:12:25 +02:00
|
|
|
exportSubscriptionsPromptNames: function () {
|
|
|
|
const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube')
|
|
|
|
const exportYouTube = this.$t('Settings.Data Settings.Export YouTube')
|
|
|
|
const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe')
|
|
|
|
return [
|
|
|
|
`${exportFreeTube} (.db)`,
|
2021-08-25 09:47:21 +02:00
|
|
|
`${exportYouTube} (.csv)`,
|
2020-11-01 22:23:06 +01:00
|
|
|
`${exportYouTube} (.json)`,
|
2020-09-07 00:12:25 +02:00
|
|
|
`${exportYouTube} (.opml)`,
|
|
|
|
`${exportNewPipe} (.json)`
|
|
|
|
]
|
2022-09-23 18:15:49 +02:00
|
|
|
},
|
2022-10-09 03:08:34 +02:00
|
|
|
primaryProfile: function () {
|
2023-08-15 20:17:10 +02:00
|
|
|
return deepCopy(this.profileList[0])
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2021-01-14 19:51:33 +01:00
|
|
|
openProfileSettings: function () {
|
|
|
|
this.$router.push({
|
|
|
|
path: '/settings/profile/'
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
importSubscriptions: async function () {
|
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
|
|
|
extensions: ['db', 'csv', 'json', 'opml', 'xml']
|
|
|
|
}
|
|
|
|
]
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
|
2022-10-25 16:44:18 +02:00
|
|
|
const response = await showOpenDialog(options)
|
2022-10-09 03:08:34 +02:00
|
|
|
if (response.canceled || response.filePaths?.length === 0) {
|
|
|
|
return
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
let textDecode
|
|
|
|
try {
|
2022-10-25 16:44:18 +02:00
|
|
|
textDecode = await readFileFromDialog(response)
|
2022-09-23 18:15:49 +02:00
|
|
|
} catch (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(`${message}: ${err}`)
|
2022-09-23 18:15:49 +02:00
|
|
|
return
|
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
response.filePaths.forEach(filePath => {
|
|
|
|
if (filePath.endsWith('.csv')) {
|
|
|
|
this.importCsvYouTubeSubscriptions(textDecode)
|
|
|
|
} else if (filePath.endsWith('.db')) {
|
|
|
|
this.importFreeTubeSubscriptions(textDecode)
|
|
|
|
} else if (filePath.endsWith('.opml') || filePath.endsWith('.xml')) {
|
|
|
|
this.importOpmlYouTubeSubscriptions(textDecode)
|
|
|
|
} else if (filePath.endsWith('.json')) {
|
|
|
|
textDecode = JSON.parse(textDecode)
|
|
|
|
if (textDecode.subscriptions) {
|
|
|
|
this.importNewPipeSubscriptions(textDecode)
|
|
|
|
} else {
|
|
|
|
this.importYouTubeSubscriptions(textDecode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2022-10-12 08:49:12 +02:00
|
|
|
importFreeTubeSubscriptions: function (textDecode) {
|
2022-09-23 18:15:49 +02:00
|
|
|
textDecode = textDecode.split('\n')
|
|
|
|
textDecode.pop()
|
|
|
|
textDecode = textDecode.map(data => JSON.parse(data))
|
|
|
|
|
|
|
|
const firstEntry = textDecode[0]
|
|
|
|
if (firstEntry.channelId && firstEntry.channelName && firstEntry.channelThumbnail && firstEntry._id && firstEntry.profile) {
|
|
|
|
// Old FreeTube subscriptions format detected, so convert it to the new one:
|
2022-10-12 08:49:12 +02:00
|
|
|
textDecode = this.convertOldFreeTubeFormatToNew(textDecode)
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2020-10-08 23:35:36 +02:00
|
|
|
|
2023-04-26 01:02:39 +02:00
|
|
|
const requiredKeys = [
|
|
|
|
'_id',
|
|
|
|
'name',
|
|
|
|
'bgColor',
|
|
|
|
'textColor',
|
|
|
|
'subscriptions'
|
|
|
|
]
|
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
textDecode.forEach((profileData) => {
|
|
|
|
// We would technically already be done by the time the data is parsed,
|
|
|
|
// however we want to limit the possibility of malicious data being sent
|
|
|
|
// to the app, so we'll only grab the data we need here.
|
2020-10-08 23:35:36 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
const profileObject = {}
|
|
|
|
Object.keys(profileData).forEach((key) => {
|
|
|
|
if (!requiredKeys.includes(key)) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unknown data key')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(`${message}: ${key}`)
|
2020-10-08 23:35:36 +02:00
|
|
|
} else {
|
2022-09-23 18:15:49 +02:00
|
|
|
profileObject[key] = profileData[key]
|
|
|
|
}
|
|
|
|
})
|
2020-10-09 03:19:29 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
if (Object.keys(profileObject).length < requiredKeys.length) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Profile object has insufficient data, skipping item')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(message)
|
2022-09-23 18:15:49 +02:00
|
|
|
} else {
|
2023-11-25 18:23:27 +01:00
|
|
|
if (profileObject._id === MAIN_PROFILE_ID) {
|
2022-10-09 03:08:34 +02:00
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => {
|
2022-09-23 18:15:49 +02:00
|
|
|
return x.name === sub.name
|
2020-10-09 03:19:29 +02:00
|
|
|
})
|
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
return profileIndex === index
|
|
|
|
})
|
2022-10-09 03:08:34 +02:00
|
|
|
this.updateProfile(this.primaryProfile)
|
2022-09-23 18:15:49 +02:00
|
|
|
} else {
|
|
|
|
const existingProfileIndex = this.profileList.findIndex((profile) => {
|
|
|
|
return profile.name.includes(profileObject.name)
|
|
|
|
})
|
2020-10-09 03:19:29 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
if (existingProfileIndex !== -1) {
|
2023-08-15 20:17:10 +02:00
|
|
|
const existingProfile = deepCopy(this.profileList[existingProfileIndex])
|
2022-09-23 18:15:49 +02:00
|
|
|
existingProfile.subscriptions = existingProfile.subscriptions.concat(profileObject.subscriptions)
|
|
|
|
existingProfile.subscriptions = existingProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = existingProfile.subscriptions.findIndex((x) => {
|
2020-10-09 03:19:29 +02:00
|
|
|
return x.name === sub.name
|
|
|
|
})
|
|
|
|
|
|
|
|
return profileIndex === index
|
|
|
|
})
|
2022-09-23 18:15:49 +02:00
|
|
|
this.updateProfile(existingProfile)
|
|
|
|
} else {
|
|
|
|
this.updateProfile(profileObject)
|
2020-10-08 23:35:36 +02:00
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => {
|
2022-09-23 18:15:49 +02:00
|
|
|
return x.name === sub.name
|
|
|
|
})
|
|
|
|
|
|
|
|
return profileIndex === index
|
|
|
|
})
|
2022-10-09 03:08:34 +02:00
|
|
|
this.updateProfile(this.primaryProfile)
|
2020-10-08 23:35:36 +02:00
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
|
|
|
})
|
2020-10-08 23:35:36 +02:00
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All subscriptions and profiles have been successfully imported'))
|
2020-10-08 23:35:36 +02:00
|
|
|
},
|
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
importCsvYouTubeSubscriptions: async function(textDecode) { // first row = header, last row = empty
|
2022-09-23 18:15:49 +02:00
|
|
|
const youtubeSubscriptions = textDecode.split('\n').filter(sub => {
|
|
|
|
return sub !== ''
|
|
|
|
})
|
|
|
|
const subscriptions = []
|
2022-10-09 03:08:34 +02:00
|
|
|
const errorList = []
|
2021-08-25 09:47:21 +02:00
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
|
2021-08-25 09:47:21 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
let count = 0
|
2021-08-25 09:47:21 +02:00
|
|
|
|
2023-04-27 03:48:10 +02:00
|
|
|
const splitCSVRegex = /(?:,|\n|^)("(?:(?:"")|[^"])*"|[^\n",]*|(?:\n|$))/g
|
2023-04-26 01:02:39 +02:00
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
const ytsubs = youtubeSubscriptions.slice(1).map(yt => {
|
|
|
|
return [...yt.matchAll(splitCSVRegex)].map(s => {
|
|
|
|
let newVal = s[1]
|
|
|
|
if (newVal.startsWith('"')) {
|
2023-04-27 03:48:10 +02:00
|
|
|
newVal = newVal.substring(1, newVal.length - 2).replaceAll('""', '"')
|
2022-10-09 03:08:34 +02:00
|
|
|
}
|
|
|
|
return newVal
|
|
|
|
})
|
|
|
|
}).filter(channel => {
|
|
|
|
return channel.length > 0
|
|
|
|
})
|
|
|
|
new Promise((resolve) => {
|
|
|
|
let finishCount = 0
|
|
|
|
ytsubs.forEach(async (yt) => {
|
|
|
|
const { subscription, result } = await this.subscribeToChannel({
|
|
|
|
channelId: yt[0],
|
|
|
|
subscriptions: subscriptions,
|
|
|
|
channelName: yt[2],
|
|
|
|
count: count++,
|
|
|
|
total: ytsubs.length
|
|
|
|
})
|
|
|
|
if (result === 1) {
|
2022-09-23 18:15:49 +02:00
|
|
|
subscriptions.push(subscription)
|
2022-10-09 03:08:34 +02:00
|
|
|
} else if (result === -1) {
|
|
|
|
errorList.push(yt)
|
2021-08-25 09:47:21 +02:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
finishCount++
|
|
|
|
if (finishCount === ytsubs.length) {
|
|
|
|
resolve(true)
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
})
|
|
|
|
}).then(_ => {
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(this.primaryProfile)
|
|
|
|
if (errorList.length !== 0) {
|
|
|
|
errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
|
|
|
|
console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
|
|
|
|
})
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
|
2022-10-09 03:08:34 +02:00
|
|
|
} else {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
}).finally(_ => {
|
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
})
|
2022-09-23 18:15:49 +02:00
|
|
|
},
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
importYouTubeSubscriptions: async function (textDecode) {
|
2022-09-23 18:15:49 +02:00
|
|
|
const subscriptions = []
|
2022-10-09 03:08:34 +02:00
|
|
|
const errorList = []
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
2021-04-21 04:43:40 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
let count = 0
|
2022-10-09 03:08:34 +02:00
|
|
|
new Promise((resolve) => {
|
|
|
|
let finishCount = 0
|
|
|
|
textDecode.forEach(async (channel) => {
|
|
|
|
const snippet = channel.snippet
|
|
|
|
if (typeof snippet === 'undefined') {
|
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(message)
|
2022-10-09 03:08:34 +02:00
|
|
|
throw new Error('Unable to find channel data')
|
2020-11-01 22:23:06 +01:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
const { subscription, result } = await this.subscribeToChannel({
|
|
|
|
channelId: snippet.resourceId.channelId,
|
|
|
|
subscriptions: subscriptions,
|
|
|
|
channelName: snippet.title,
|
|
|
|
thumbnail: snippet.thumbnails.default.url,
|
|
|
|
count: count++,
|
|
|
|
total: textDecode.length
|
|
|
|
})
|
|
|
|
if (result === 1) {
|
|
|
|
subscriptions.push(subscription)
|
|
|
|
} else if (result === -1) {
|
|
|
|
errorList.push([snippet.resourceId.channelId, `https://www.youtube.com/channel/${snippet.resourceId.channelId}`, snippet.title])
|
2020-11-01 22:23:06 +01:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
finishCount++
|
|
|
|
if (finishCount === textDecode.length) {
|
|
|
|
resolve(true)
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
2021-05-22 01:49:48 +02:00
|
|
|
})
|
2022-10-09 03:08:34 +02:00
|
|
|
}).then(_ => {
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(this.primaryProfile)
|
|
|
|
if (errorList.length !== 0) {
|
|
|
|
errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
|
|
|
|
console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
|
|
|
|
})
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
|
2022-10-09 03:08:34 +02:00
|
|
|
} else {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
|
2022-10-09 03:08:34 +02:00
|
|
|
}
|
|
|
|
}).finally(_ => {
|
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
})
|
|
|
|
},
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
importOpmlYouTubeSubscriptions: async function (data) {
|
2022-11-15 09:10:51 +01:00
|
|
|
let xmlDom
|
|
|
|
const domParser = new DOMParser()
|
2022-09-23 18:15:49 +02:00
|
|
|
try {
|
2022-11-15 09:10:51 +01:00
|
|
|
xmlDom = domParser.parseFromString(data, 'application/xml')
|
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling
|
|
|
|
const errorNode = xmlDom.querySelector('parsererror')
|
|
|
|
if (errorNode) {
|
|
|
|
throw errorNode.textContent
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
} catch (err) {
|
2022-11-15 09:10:51 +01:00
|
|
|
console.error('error reading OPML subscriptions file, falling back to HTML parser...')
|
2022-09-23 18:15:49 +02:00
|
|
|
console.error(err)
|
2022-11-15 09:10:51 +01:00
|
|
|
// try parsing with the html parser instead which is more lenient
|
|
|
|
try {
|
|
|
|
const htmlDom = domParser.parseFromString(data, 'text/html')
|
|
|
|
|
|
|
|
xmlDom = htmlDom
|
|
|
|
} catch {
|
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
showToast(`${message}: ${err}`)
|
|
|
|
return
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
const feedData = xmlDom.querySelectorAll('body outline[xmlUrl]')
|
|
|
|
if (feedData.length === 0) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
showToast(message)
|
|
|
|
return
|
|
|
|
}
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
const subscriptions = []
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
let count = 0
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
feedData.forEach(async (channel) => {
|
|
|
|
const xmlUrl = channel.getAttribute('xmlUrl')
|
|
|
|
let channelId
|
|
|
|
if (xmlUrl.includes('https://www.youtube.com/feeds/videos.xml?channel_id=')) {
|
|
|
|
channelId = new URL(xmlUrl).searchParams.get('channel_id')
|
|
|
|
} else if (xmlUrl.includes('/feed/channel/')) {
|
|
|
|
// handle invidious exports https://yewtu.be/feed/channel/{CHANNELID}
|
|
|
|
channelId = new URL(xmlUrl).pathname.split('/').filter(part => part).at(-1)
|
|
|
|
} else {
|
|
|
|
console.error(`Unknown xmlUrl format: ${xmlUrl}`)
|
|
|
|
}
|
|
|
|
const subExists = this.primaryProfile.subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === channelId
|
|
|
|
})
|
|
|
|
if (subExists === -1) {
|
|
|
|
let channelInfo
|
|
|
|
if (this.backendPreference === 'invidious') {
|
|
|
|
channelInfo = await this.getChannelInfoInvidious(channelId)
|
|
|
|
} else {
|
|
|
|
channelInfo = await this.getChannelInfoLocal(channelId)
|
|
|
|
}
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
if (typeof channelInfo.author !== 'undefined') {
|
|
|
|
const subscription = {
|
|
|
|
id: channelId,
|
|
|
|
name: channelInfo.author,
|
|
|
|
thumbnail: channelInfo.authorThumbnails[1].url
|
2021-05-22 01:49:48 +02:00
|
|
|
}
|
2022-11-15 09:10:51 +01:00
|
|
|
subscriptions.push(subscription)
|
2021-05-22 01:49:48 +02:00
|
|
|
}
|
2022-11-15 09:10:51 +01:00
|
|
|
}
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
count++
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
const progressPercentage = (count / feedData.length) * 100
|
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
if (count === feedData.length) {
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(this.primaryProfile)
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-11-15 09:10:51 +01:00
|
|
|
if (subscriptions.length < count) {
|
|
|
|
showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
|
|
|
|
} else {
|
|
|
|
showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
|
2021-05-22 01:49:48 +02:00
|
|
|
}
|
2022-11-15 09:10:51 +01:00
|
|
|
|
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
}
|
|
|
|
})
|
2022-09-23 18:15:49 +02:00
|
|
|
},
|
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
importNewPipeSubscriptions: async function (newPipeData) {
|
2022-09-23 18:15:49 +02:00
|
|
|
if (typeof newPipeData.subscriptions === 'undefined') {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.Invalid subscriptions file'))
|
2022-09-23 18:15:49 +02:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const newPipeSubscriptions = newPipeData.subscriptions.filter((channel, index) => {
|
2023-08-01 09:47:52 +02:00
|
|
|
return new URL(channel.url).hostname === 'www.youtube.com'
|
2022-09-23 18:15:49 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
const subscriptions = []
|
2022-10-09 03:08:34 +02:00
|
|
|
const errorList = []
|
2022-09-23 18:15:49 +02:00
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.This might take a while, please wait'))
|
2022-09-23 18:15:49 +02:00
|
|
|
|
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
new Promise((resolve) => {
|
|
|
|
let finishCount = 0
|
|
|
|
newPipeSubscriptions.forEach(async (channel, index) => {
|
|
|
|
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
|
|
|
|
const { subscription, result } = await this.subscribeToChannel({
|
|
|
|
channelId: channelId,
|
|
|
|
subscriptions: subscriptions,
|
|
|
|
channelName: channel.name,
|
|
|
|
count: count++,
|
|
|
|
total: newPipeSubscriptions.length
|
|
|
|
})
|
|
|
|
if (result === 1) {
|
2022-09-23 18:15:49 +02:00
|
|
|
subscriptions.push(subscription)
|
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
if (result === -1) {
|
|
|
|
errorList.push([channelId, channel.url, channel.name])
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
finishCount++
|
|
|
|
if (finishCount === newPipeSubscriptions.length) {
|
|
|
|
resolve(true)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}).then(_ => {
|
|
|
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(this.primaryProfile)
|
|
|
|
if (errorList.count > 0) {
|
|
|
|
errorList.forEach(e => { // log it to console for now, dedicated tab for 'error' channels needed
|
|
|
|
console.error(`failed to import ${e[2]}. Url to channel: ${e[1]}.`)
|
|
|
|
})
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported'))
|
2022-10-09 03:08:34 +02:00
|
|
|
} else {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All subscriptions have been successfully imported'))
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
2022-10-09 03:08:34 +02:00
|
|
|
}).finally(_ => {
|
|
|
|
this.updateShowProgressBar(false)
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
exportSubscriptions: function (option) {
|
|
|
|
this.showExportSubscriptionsPrompt = false
|
|
|
|
|
|
|
|
if (option === null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (option) {
|
|
|
|
case 'freetube':
|
|
|
|
this.exportFreeTubeSubscriptions()
|
|
|
|
break
|
2021-08-25 09:47:21 +02:00
|
|
|
case 'youtubenew':
|
|
|
|
this.exportCsvYouTubeSubscriptions()
|
|
|
|
break
|
2020-09-07 00:12:25 +02:00
|
|
|
case 'youtube':
|
|
|
|
this.exportYouTubeSubscriptions()
|
|
|
|
break
|
2020-11-01 22:23:06 +01:00
|
|
|
case 'youtubeold':
|
|
|
|
this.exportOpmlYouTubeSubscriptions()
|
|
|
|
break
|
2020-09-07 00:12:25 +02:00
|
|
|
case 'newpipe':
|
|
|
|
this.exportNewPipeSubscriptions()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2021-04-21 06:09:06 +02:00
|
|
|
exportFreeTubeSubscriptions: async function () {
|
2022-10-19 07:50:21 +02:00
|
|
|
const subscriptionsDb = this.profileList.map((profile) => {
|
|
|
|
return JSON.stringify(profile)
|
|
|
|
}).join('\n') + '\n'// a trailing line is expected
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'freetube-subscriptions-' + dateStr + '.db'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
2020-09-07 00:12:25 +02:00
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, subscriptionsDb, 'Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
exportYouTubeSubscriptions: async function () {
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + dateStr + '.json'
|
2020-11-01 22:23:06 +01:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
2020-11-01 22:23:06 +01:00
|
|
|
extensions: ['json']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const subscriptionsObject = this.profileList[0].subscriptions.map((channel) => {
|
|
|
|
const object = {
|
|
|
|
contentDetails: {
|
|
|
|
activityType: 'all',
|
|
|
|
newItemCount: 0,
|
|
|
|
totalItemCount: 0
|
|
|
|
},
|
|
|
|
etag: '',
|
|
|
|
id: '',
|
|
|
|
kind: 'youtube#subscription',
|
|
|
|
snippet: {
|
|
|
|
channelId: channel.id,
|
|
|
|
description: '',
|
|
|
|
publishedAt: new Date(),
|
|
|
|
resourceId: {
|
|
|
|
channelId: channel.id,
|
|
|
|
kind: 'youtube#channel'
|
|
|
|
},
|
|
|
|
thumbnails: {
|
|
|
|
default: {
|
|
|
|
url: channel.thumbnail
|
|
|
|
},
|
|
|
|
high: {
|
|
|
|
url: channel.thumbnail
|
|
|
|
},
|
|
|
|
medium: {
|
|
|
|
url: channel.thumbnail
|
|
|
|
}
|
|
|
|
},
|
|
|
|
title: channel.name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return object
|
|
|
|
})
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, JSON.stringify(subscriptionsObject), 'Subscriptions have been successfully exported')
|
2020-11-01 22:23:06 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
exportOpmlYouTubeSubscriptions: async function () {
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + dateStr + '.opml'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
2020-09-07 00:12:25 +02:00
|
|
|
extensions: ['opml']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
let opmlData = '<opml version="1.1"><body><outline text="YouTube Subscriptions" title="YouTube Subscriptions">'
|
|
|
|
|
|
|
|
this.profileList[0].subscriptions.forEach((channel) => {
|
2023-06-19 19:01:12 +02:00
|
|
|
const escapedName = escapeHTML(channel.name)
|
2022-12-27 03:14:23 +01:00
|
|
|
|
|
|
|
const channelOpmlString = `<outline text="${escapedName}" title="${escapedName}" type="rss" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}"/>`
|
2020-09-07 00:12:25 +02:00
|
|
|
opmlData += channelOpmlString
|
|
|
|
})
|
|
|
|
|
2022-12-27 03:14:23 +01:00
|
|
|
opmlData += '</outline></body></opml>'
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, opmlData, 'Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
exportCsvYouTubeSubscriptions: async function () {
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + dateStr + '.csv'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
2021-08-25 09:47:21 +02:00
|
|
|
extensions: ['csv']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
let exportText = 'Channel ID,Channel URL,Channel title\n'
|
|
|
|
this.profileList[0].subscriptions.forEach((channel) => {
|
|
|
|
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
2022-05-25 10:28:18 +02:00
|
|
|
let channelName = channel.name
|
2022-10-09 03:08:34 +02:00
|
|
|
if (channelName.search(',') !== -1) { // add quotations and escape existing quotations if channel has comma in name
|
|
|
|
channelName = `"${channelName.replaceAll('"', '""')}"`
|
2022-05-25 10:28:18 +02:00
|
|
|
}
|
|
|
|
exportText += `${channel.id},${channelUrl},${channelName}\n`
|
2021-08-25 09:47:21 +02:00
|
|
|
})
|
|
|
|
exportText += '\n'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, exportText, 'Subscriptions have been successfully exported')
|
2021-08-25 09:47:21 +02:00
|
|
|
},
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
exportNewPipeSubscriptions: async function () {
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'newpipe-subscriptions-' + dateStr + '.json'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
2020-09-07 00:12:25 +02:00
|
|
|
extensions: ['json']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const newPipeObject = {
|
|
|
|
app_version: '0.19.8',
|
|
|
|
app_version_int: 953,
|
|
|
|
subscriptions: []
|
|
|
|
}
|
|
|
|
|
|
|
|
this.profileList[0].subscriptions.forEach((channel) => {
|
|
|
|
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
|
|
|
const subscription = {
|
|
|
|
service_id: 0,
|
|
|
|
url: channelUrl,
|
|
|
|
name: channel.name
|
|
|
|
}
|
|
|
|
|
|
|
|
newPipeObject.subscriptions.push(subscription)
|
|
|
|
})
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, JSON.stringify(newPipeObject), 'Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
importHistory: async function () {
|
2020-09-07 00:12:25 +02:00
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.History File'),
|
2023-03-30 17:51:37 +02:00
|
|
|
extensions: ['db', 'json']
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-10-25 16:44:18 +02:00
|
|
|
const response = await showOpenDialog(options)
|
2022-09-23 18:15:49 +02:00
|
|
|
if (response.canceled || response.filePaths?.length === 0) {
|
2021-05-22 01:49:48 +02:00
|
|
|
return
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
let textDecode
|
|
|
|
try {
|
2022-10-25 16:44:18 +02:00
|
|
|
textDecode = await readFileFromDialog(response)
|
2022-09-23 18:15:49 +02:00
|
|
|
} catch (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(`${message}: ${err}`)
|
2022-09-23 18:15:49 +02:00
|
|
|
return
|
|
|
|
}
|
2023-03-30 17:51:37 +02:00
|
|
|
|
|
|
|
response.filePaths.forEach(filePath => {
|
|
|
|
if (filePath.endsWith('.db')) {
|
2023-05-29 03:57:31 +02:00
|
|
|
this.importFreeTubeHistory(textDecode.split('\n'))
|
2023-03-30 17:51:37 +02:00
|
|
|
} else if (filePath.endsWith('.json')) {
|
|
|
|
this.importYouTubeHistory(JSON.parse(textDecode))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
importFreeTubeHistory(textDecode) {
|
2022-09-23 18:15:49 +02:00
|
|
|
textDecode.pop()
|
|
|
|
|
2023-04-26 01:02:39 +02:00
|
|
|
const requiredKeys = [
|
|
|
|
'author',
|
|
|
|
'authorId',
|
|
|
|
'description',
|
|
|
|
'isLive',
|
|
|
|
'lengthSeconds',
|
|
|
|
'published',
|
|
|
|
'timeWatched',
|
|
|
|
'title',
|
|
|
|
'type',
|
|
|
|
'videoId',
|
|
|
|
'viewCount',
|
2023-09-29 19:16:00 +02:00
|
|
|
'watchProgress',
|
|
|
|
]
|
|
|
|
|
|
|
|
const optionalKeys = [
|
|
|
|
// `_id` absent if marked as watched manually
|
|
|
|
'_id',
|
|
|
|
'lastViewedPlaylistId',
|
|
|
|
]
|
|
|
|
|
|
|
|
const ignoredKeys = [
|
|
|
|
'paid',
|
2023-04-26 01:02:39 +02:00
|
|
|
]
|
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
textDecode.forEach((history) => {
|
|
|
|
const historyData = JSON.parse(history)
|
|
|
|
// We would technically already be done by the time the data is parsed,
|
|
|
|
// however we want to limit the possibility of malicious data being sent
|
|
|
|
// to the app, so we'll only grab the data we need here.
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
const historyObject = {}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
Object.keys(historyData).forEach((key) => {
|
2023-09-29 19:16:00 +02:00
|
|
|
if (requiredKeys.includes(key) || optionalKeys.includes(key)) {
|
2022-09-23 18:15:49 +02:00
|
|
|
historyObject[key] = historyData[key]
|
2023-09-29 19:16:00 +02:00
|
|
|
} else if (!ignoredKeys.includes(key)) {
|
|
|
|
showToast(`Unknown data key: ${key}`)
|
2021-05-22 01:49:48 +02:00
|
|
|
}
|
2023-09-29 19:16:00 +02:00
|
|
|
// Else do not import the key
|
2021-05-22 01:49:48 +02:00
|
|
|
})
|
|
|
|
|
2023-09-29 19:16:00 +02:00
|
|
|
const historyObjectKeysSet = new Set(Object.keys(historyObject))
|
|
|
|
const missingKeys = requiredKeys.filter(x => !historyObjectKeysSet.has(x))
|
|
|
|
if (missingKeys.length > 0) {
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.History object has insufficient data, skipping item'))
|
2023-09-29 19:16:00 +02:00
|
|
|
console.error('Missing Keys: ', missingKeys, historyData)
|
2022-09-23 18:15:49 +02:00
|
|
|
} else {
|
|
|
|
this.updateHistory(historyObject)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All watched history has been successfully imported'))
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2023-03-30 17:51:37 +02:00
|
|
|
importYouTubeHistory(historyData) {
|
|
|
|
const filterPredicate = item =>
|
|
|
|
item.products.includes('YouTube') &&
|
|
|
|
item.titleUrl != null && // removed video doesnt contain url...
|
|
|
|
item.titleUrl.includes('www.youtube.com/watch?v') &&
|
|
|
|
item.details == null // dont import ads
|
|
|
|
|
|
|
|
const filteredHistoryData = historyData.filter(filterPredicate)
|
|
|
|
|
|
|
|
// remove 'Watched' and translated variants from start of title
|
|
|
|
// so we get the common string prefix for all the titles
|
|
|
|
const getCommonStart = (allTitles) => {
|
|
|
|
const watchedTitle = allTitles[0].split(' ')
|
|
|
|
allTitles.forEach((title) => {
|
|
|
|
const splitTitle = title.split(' ')
|
|
|
|
for (let wtIndex = 0; wtIndex <= watchedTitle.length; wtIndex++) {
|
|
|
|
if (!splitTitle.includes(watchedTitle[wtIndex])) {
|
|
|
|
watchedTitle.splice(wtIndex, watchedTitle.length - wtIndex)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return watchedTitle.join(' ')
|
|
|
|
}
|
|
|
|
|
|
|
|
const commonStart = getCommonStart(filteredHistoryData.map(e => e.title))
|
|
|
|
// We would technically already be done by the time the data is parsed,
|
|
|
|
// however we want to limit the possibility of malicious data being sent
|
|
|
|
// to the app, so we'll only grab the data we need here.
|
|
|
|
|
|
|
|
const keyMapping = {
|
|
|
|
title: [{ importKey: 'title', predicate: item => item.slice(commonStart.length) }], // Removes the "Watched " term on the title
|
|
|
|
titleUrl: [{ importKey: 'videoId', predicate: item => item.replaceAll(/https:\/\/www\.youtube\.com\/watch\?v=/gi, '') }], // Extracts the video ID
|
|
|
|
time: [{ importKey: 'timeWatched', predicate: item => new Date(item).valueOf() }],
|
|
|
|
subtitles: [
|
|
|
|
{ importKey: 'author', predicate: item => item[0].name ?? '' },
|
|
|
|
{ importKey: 'authorId', predicate: item => item[0].url?.replaceAll(/https:\/\/www\.youtube\.com\/channel\//gi, '') ?? '' },
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
const knownKeys = [
|
|
|
|
'header',
|
|
|
|
'description',
|
|
|
|
'products',
|
|
|
|
'details',
|
|
|
|
'activityControls',
|
|
|
|
].concat(Object.keys(keyMapping))
|
|
|
|
|
|
|
|
filteredHistoryData.forEach(element => {
|
|
|
|
const historyObject = {}
|
|
|
|
|
|
|
|
Object.keys(element).forEach((key) => {
|
|
|
|
if (!knownKeys.includes(key)) {
|
|
|
|
showToast(`Unknown data key: ${key}`)
|
|
|
|
} else {
|
|
|
|
const mapping = keyMapping[key]
|
|
|
|
|
|
|
|
if (mapping && Array.isArray(mapping)) {
|
|
|
|
mapping.forEach(item => {
|
|
|
|
historyObject[item.importKey] = item.predicate(element[key])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (Object.keys(historyObject).length < keyMapping.length - 1) {
|
|
|
|
showToast(this.$t('Settings.Data Settings.History object has insufficient data, skipping item'))
|
|
|
|
} else {
|
|
|
|
// YouTube history export does not have this data, setting some defaults.
|
|
|
|
historyObject.type = 'video'
|
|
|
|
historyObject.published = historyObject.timeWatched ?? 1
|
|
|
|
historyObject.description = ''
|
|
|
|
historyObject.lengthSeconds = null
|
|
|
|
historyObject.watchProgress = 1
|
|
|
|
historyObject.isLive = false
|
|
|
|
|
|
|
|
this.updateHistory(historyObject)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
showToast(this.$t('Settings.Data Settings.All watched history has been successfully imported'))
|
|
|
|
},
|
|
|
|
|
2021-04-21 06:09:06 +02:00
|
|
|
exportHistory: async function () {
|
2023-09-14 03:31:07 +02:00
|
|
|
const historyDb = this.historyCacheSorted.map((historyEntry) => {
|
2022-10-19 07:50:21 +02:00
|
|
|
return JSON.stringify(historyEntry)
|
|
|
|
}).join('\n') + '\n'
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'freetube-history-' + dateStr + '.db'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Playlist File'),
|
2020-09-07 00:12:25 +02:00
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
await this.promptAndWriteToFile(options, historyDb, 'All watched history has been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2022-02-06 20:31:27 +01:00
|
|
|
importPlaylists: async function () {
|
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
2022-10-09 03:08:34 +02:00
|
|
|
name: this.$t('Settings.Data Settings.Playlist File'),
|
2022-02-06 20:31:27 +01:00
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2022-10-25 16:44:18 +02:00
|
|
|
const response = await showOpenDialog(options)
|
2022-09-23 18:15:49 +02:00
|
|
|
if (response.canceled || response.filePaths?.length === 0) {
|
2022-02-06 20:31:27 +01:00
|
|
|
return
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
let data
|
|
|
|
try {
|
2022-10-25 16:44:18 +02:00
|
|
|
data = await readFileFromDialog(response)
|
2022-09-23 18:15:49 +02:00
|
|
|
} catch (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(`${message}: ${err}`)
|
2022-09-23 18:15:49 +02:00
|
|
|
return
|
|
|
|
}
|
2024-03-07 16:51:04 +01:00
|
|
|
let playlists = null
|
|
|
|
|
|
|
|
// for the sake of backwards compatibility,
|
|
|
|
// check if this is the old JSON array export (used until version 0.19.1),
|
|
|
|
// that didn't match the actual database format
|
|
|
|
const trimmedData = data.trim()
|
|
|
|
|
|
|
|
if (trimmedData[0] === '[' && trimmedData[trimmedData.length - 1] === ']') {
|
|
|
|
playlists = JSON.parse(trimmedData)
|
|
|
|
} else {
|
|
|
|
// otherwise assume this is the correct database format,
|
|
|
|
// which is also what we export now (used in 0.20.0 and later versions)
|
|
|
|
data = data.split('\n')
|
|
|
|
data.pop()
|
|
|
|
|
|
|
|
playlists = data.map(playlistJson => JSON.parse(playlistJson))
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
|
2023-04-26 01:02:39 +02:00
|
|
|
const requiredKeys = [
|
|
|
|
'playlistName',
|
2024-01-03 19:44:57 +01:00
|
|
|
'videos',
|
2023-04-26 01:02:39 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
const optionalKeys = [
|
2024-01-03 19:44:57 +01:00
|
|
|
'description',
|
|
|
|
'createdAt',
|
|
|
|
]
|
|
|
|
|
|
|
|
const ignoredKeys = [
|
2023-04-26 01:02:39 +02:00
|
|
|
'_id',
|
2024-01-03 19:44:57 +01:00
|
|
|
'title',
|
|
|
|
'type',
|
2023-04-26 01:02:39 +02:00
|
|
|
'protected',
|
2024-01-03 19:44:57 +01:00
|
|
|
'lastUpdatedAt',
|
|
|
|
'lastPlayedAt',
|
|
|
|
'removeOnWatched',
|
|
|
|
|
|
|
|
'thumbnail',
|
|
|
|
'channelName',
|
|
|
|
'channelId',
|
|
|
|
'playlistId',
|
|
|
|
'videoCount',
|
2023-04-26 01:02:39 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
const requiredVideoKeys = [
|
|
|
|
'videoId',
|
|
|
|
'title',
|
|
|
|
'author',
|
|
|
|
'authorId',
|
|
|
|
'lengthSeconds',
|
|
|
|
'timeAdded',
|
2024-01-03 19:44:57 +01:00
|
|
|
|
|
|
|
// `playlistItemId` should be optional for backward compatibility
|
|
|
|
// 'playlistItemId',
|
2023-04-26 01:02:39 +02:00
|
|
|
]
|
|
|
|
|
2024-01-03 19:44:57 +01:00
|
|
|
playlists.forEach((playlistData) => {
|
2022-09-23 18:15:49 +02:00
|
|
|
// We would technically already be done by the time the data is parsed,
|
|
|
|
// however we want to limit the possibility of malicious data being sent
|
|
|
|
// to the app, so we'll only grab the data we need here.
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
const playlistObject = {}
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
Object.keys(playlistData).forEach((key) => {
|
2024-01-03 19:44:57 +01:00
|
|
|
if ([requiredKeys, optionalKeys, ignoredKeys].every((ks) => !ks.includes(key))) {
|
2022-09-23 18:15:49 +02:00
|
|
|
const message = `${this.$t('Settings.Data Settings.Unknown data key')}: ${key}`
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(message)
|
2022-09-23 18:15:49 +02:00
|
|
|
} else if (key === 'videos') {
|
|
|
|
const videoArray = []
|
|
|
|
playlistData.videos.forEach((video) => {
|
2024-01-03 19:44:57 +01:00
|
|
|
const videoPropertyKeys = Object.keys(video)
|
|
|
|
const videoObjectHasAllRequiredKeys = requiredVideoKeys.every((k) => videoPropertyKeys.includes(k))
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2024-01-03 19:44:57 +01:00
|
|
|
if (videoObjectHasAllRequiredKeys) {
|
2022-09-23 18:15:49 +02:00
|
|
|
videoArray.push(video)
|
|
|
|
}
|
2022-02-06 20:31:27 +01:00
|
|
|
})
|
2022-09-23 18:15:49 +02:00
|
|
|
|
|
|
|
playlistObject[key] = videoArray
|
2024-01-03 19:44:57 +01:00
|
|
|
} else if (!ignoredKeys.includes(key)) {
|
|
|
|
// Do nothing for keys to be ignored
|
2022-09-23 18:15:49 +02:00
|
|
|
playlistObject[key] = playlistData[key]
|
|
|
|
}
|
|
|
|
})
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2024-01-03 19:44:57 +01:00
|
|
|
const playlistObjectKeys = Object.keys(playlistObject)
|
|
|
|
const playlistObjectHasAllRequiredKeys = requiredKeys.every((k) => playlistObjectKeys.includes(k))
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2024-01-03 19:44:57 +01:00
|
|
|
if (playlistObjectHasAllRequiredKeys) {
|
2022-09-23 18:15:49 +02:00
|
|
|
const existingPlaylist = this.allPlaylists.find((playlist) => {
|
|
|
|
return playlist.playlistName === playlistObject.playlistName
|
|
|
|
})
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2022-09-23 18:15:49 +02:00
|
|
|
if (existingPlaylist !== undefined) {
|
|
|
|
playlistObject.videos.forEach((video) => {
|
2024-01-03 19:44:57 +01:00
|
|
|
let videoExists = false
|
|
|
|
if (video.playlistItemId != null) {
|
|
|
|
// Find by `playlistItemId` if present
|
|
|
|
videoExists = existingPlaylist.videos.some((x) => {
|
|
|
|
// Allow duplicate (by videoId) videos to be added
|
|
|
|
return x.videoId === video.videoId && x.playlistItemId === video.playlistItemId
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Older playlist exports have no `playlistItemId` but have `timeAdded`
|
|
|
|
// Which might be duplicate for copied playlists with duplicate `videoId`
|
|
|
|
videoExists = existingPlaylist.videos.some((x) => {
|
|
|
|
// Allow duplicate (by videoId) videos to be added
|
|
|
|
return x.videoId === video.videoId && x.timeAdded === video.timeAdded
|
|
|
|
})
|
|
|
|
}
|
2022-09-23 18:15:49 +02:00
|
|
|
|
2023-04-26 01:02:39 +02:00
|
|
|
if (!videoExists) {
|
2024-01-03 19:44:57 +01:00
|
|
|
// Keep original `timeAdded` value
|
2022-09-23 18:15:49 +02:00
|
|
|
const payload = {
|
2024-01-03 19:44:57 +01:00
|
|
|
_id: existingPlaylist._id,
|
|
|
|
videoData: video,
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.addVideo(payload)
|
|
|
|
}
|
|
|
|
})
|
2024-01-03 19:44:57 +01:00
|
|
|
// Update playlist's `lastUpdatedAt`
|
|
|
|
this.updatePlaylist({ _id: existingPlaylist._id })
|
2022-09-23 18:15:49 +02:00
|
|
|
} else {
|
|
|
|
this.addPlaylist(playlistObject)
|
2022-02-06 20:31:27 +01:00
|
|
|
}
|
2024-01-03 19:44:57 +01:00
|
|
|
} else {
|
|
|
|
const message = this.$t('Settings.Data Settings.Playlist insufficient data', { playlist: playlistData.playlistName })
|
|
|
|
showToast(message)
|
2022-09-23 18:15:49 +02:00
|
|
|
}
|
|
|
|
})
|
2022-02-06 20:31:27 +01:00
|
|
|
|
2022-10-14 07:59:49 +02:00
|
|
|
showToast(this.$t('Settings.Data Settings.All playlists has been successfully imported'))
|
2022-02-06 20:31:27 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
exportPlaylists: async function () {
|
2023-03-02 22:11:36 +01:00
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'freetube-playlists-' + dateStr + '.db'
|
2022-02-06 20:31:27 +01:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2024-03-07 16:51:04 +01:00
|
|
|
const playlistsDb = this.allPlaylists.map(playlist => {
|
|
|
|
return JSON.stringify(playlist)
|
|
|
|
}).join('\n') + '\n'// a trailing line is expected
|
|
|
|
|
|
|
|
await this.promptAndWriteToFile(options, playlistsDb, 'All playlists has been successfully exported')
|
2022-02-06 20:31:27 +01:00
|
|
|
},
|
|
|
|
|
2024-01-03 19:44:57 +01:00
|
|
|
exportPlaylistsForOlderVersionsSometimes: function () {
|
|
|
|
if (this.shouldExportPlaylistForOlderVersions) {
|
|
|
|
this.exportPlaylistsForOlderVersions()
|
|
|
|
} else {
|
|
|
|
this.exportPlaylists()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
exportPlaylistsForOlderVersions: async function () {
|
|
|
|
const dateStr = getTodayDateStrLocalTimezone()
|
|
|
|
const exportFileName = 'freetube-playlists-as-single-favorites-playlist-' + dateStr + '.db'
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const favoritesPlaylistData = {
|
|
|
|
playlistName: 'Favorites',
|
|
|
|
protected: true,
|
|
|
|
videos: [],
|
|
|
|
}
|
|
|
|
|
|
|
|
this.allPlaylists.forEach((playlist) => {
|
|
|
|
playlist.videos.forEach((video) => {
|
|
|
|
const videoAlreadyAdded = favoritesPlaylistData.videos.some((v) => {
|
|
|
|
return v.videoId === video.videoId
|
|
|
|
})
|
|
|
|
if (videoAlreadyAdded) { return }
|
|
|
|
|
|
|
|
favoritesPlaylistData.videos.push(
|
|
|
|
Object.assign({
|
|
|
|
// The "required" keys during import (but actually unused) in older versions
|
|
|
|
isLive: false,
|
|
|
|
paid: false,
|
|
|
|
published: '',
|
|
|
|
}, video)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
await this.promptAndWriteToFile(options, JSON.stringify([favoritesPlaylistData]), 'All playlists has been successfully exported')
|
|
|
|
},
|
|
|
|
|
2022-10-12 08:49:12 +02:00
|
|
|
convertOldFreeTubeFormatToNew(oldData) {
|
2020-09-12 10:00:18 +02:00
|
|
|
const convertedData = []
|
|
|
|
for (const channel of oldData) {
|
2020-10-07 19:23:18 +02:00
|
|
|
const listOfProfilesAlreadyAdded = []
|
2020-09-12 10:00:18 +02:00
|
|
|
for (const profile of channel.profile) {
|
|
|
|
let index = convertedData.findIndex(p => p.name === profile.value)
|
|
|
|
if (index === -1) { // profile doesn't exist yet
|
2022-10-12 08:49:12 +02:00
|
|
|
const randomBgColor = getRandomColor()
|
2022-10-10 09:45:18 +02:00
|
|
|
const contrastyTextColor = calculateColorLuminance(randomBgColor)
|
2020-09-12 10:00:18 +02:00
|
|
|
convertedData.push({
|
|
|
|
name: profile.value,
|
|
|
|
bgColor: randomBgColor,
|
|
|
|
textColor: contrastyTextColor,
|
|
|
|
subscriptions: [],
|
|
|
|
_id: channel._id
|
|
|
|
})
|
|
|
|
index = convertedData.length - 1
|
2020-10-07 19:23:18 +02:00
|
|
|
} else if (listOfProfilesAlreadyAdded.indexOf(index) !== -1) {
|
|
|
|
continue
|
2020-09-12 10:00:18 +02:00
|
|
|
}
|
2020-10-07 19:23:18 +02:00
|
|
|
listOfProfilesAlreadyAdded.push(index)
|
2020-09-12 10:00:18 +02:00
|
|
|
convertedData[index].subscriptions.push({
|
|
|
|
id: channel.channelId,
|
|
|
|
name: channel.channelName,
|
|
|
|
thumbnail: channel.channelThumbnail
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return convertedData
|
|
|
|
},
|
|
|
|
|
2023-02-09 07:28:41 +01:00
|
|
|
promptAndWriteToFile: async function (saveOptions, content, successMessageKeySuffix) {
|
|
|
|
const response = await showSaveDialog(saveOptions)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await writeFileFromDialog(response, content)
|
|
|
|
} catch (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
|
|
|
showToast(`${message}: ${writeErr}`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
showToast(this.$t(`Settings.Data Settings.${successMessageKeySuffix}`))
|
|
|
|
},
|
|
|
|
|
2020-09-07 00:12:25 +02:00
|
|
|
getChannelInfoInvidious: function (channelId) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const subscriptionsPayload = {
|
|
|
|
resource: 'channels',
|
|
|
|
id: channelId,
|
|
|
|
params: {}
|
|
|
|
}
|
|
|
|
|
2023-01-12 07:55:21 +01:00
|
|
|
invidiousAPICall(subscriptionsPayload).then((response) => {
|
2020-09-07 00:12:25 +02:00
|
|
|
resolve(response)
|
|
|
|
}).catch((err) => {
|
|
|
|
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
2023-08-01 09:47:52 +02:00
|
|
|
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
|
|
|
copyToClipboard(err)
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
|
2023-01-05 04:53:39 +01:00
|
|
|
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
2024-02-16 22:20:22 +01:00
|
|
|
showToast(this.$t('Falling back to Local API'))
|
2020-09-07 00:12:25 +02:00
|
|
|
resolve(this.getChannelInfoLocal(channelId))
|
|
|
|
} else {
|
|
|
|
resolve([])
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2023-03-01 01:39:33 +01:00
|
|
|
getChannelInfoLocal: async function (channelId) {
|
|
|
|
try {
|
|
|
|
const channel = await getLocalChannel(channelId)
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2023-03-01 01:39:33 +01:00
|
|
|
if (channel.alert) {
|
2023-08-01 09:47:52 +02:00
|
|
|
return []
|
2023-03-01 01:39:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
author: channel.header.author.name,
|
|
|
|
authorThumbnails: channel.header.author.thumbnails
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
|
|
|
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
|
|
|
copyToClipboard(err)
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
2023-03-01 01:39:33 +01:00
|
|
|
|
|
|
|
if (this.backendFallback && this.backendPreference === 'local') {
|
|
|
|
showToast(this.$t('Falling back to the Invidious API'))
|
|
|
|
return await this.getChannelInfoInvidious(channelId)
|
|
|
|
} else {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2022-10-09 03:08:34 +02:00
|
|
|
/*
|
|
|
|
TODO: allow default thumbnail to be used to limit requests to YouTube
|
|
|
|
(thumbnail will get updated when user goes to their channel page)
|
|
|
|
Returns:
|
|
|
|
-1: an error occured
|
|
|
|
0: already subscribed
|
|
|
|
1: successfully subscribed
|
|
|
|
*/
|
|
|
|
async subscribeToChannel({ channelId, subscriptions, channelName = null, thumbnail = null, count = 0, total = 0 }) {
|
|
|
|
let result = 1
|
|
|
|
if (this.isChannelSubscribed(channelId, subscriptions)) {
|
|
|
|
return { subscription: null, successMessage: 0 }
|
|
|
|
}
|
|
|
|
|
|
|
|
let channelInfo
|
|
|
|
let subscription = null
|
|
|
|
if (channelName === null || thumbnail === null) {
|
|
|
|
try {
|
|
|
|
if (this.backendPreference === 'invidious') {
|
|
|
|
channelInfo = await this.getChannelInfoInvidious(channelId)
|
|
|
|
} else {
|
|
|
|
channelInfo = await this.getChannelInfoLocal(channelId)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
result = -1
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
channelInfo = { author: channelName, authorThumbnails: [null, { url: thumbnail }] }
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof channelInfo.author !== 'undefined') {
|
|
|
|
subscription = {
|
|
|
|
id: channelId,
|
|
|
|
name: channelInfo.author,
|
|
|
|
thumbnail: channelInfo.authorThumbnails[1].url
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result = -1
|
|
|
|
}
|
|
|
|
const progressPercentage = (count / (total - 1)) * 100
|
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
|
|
|
return { subscription, result }
|
|
|
|
},
|
|
|
|
|
|
|
|
isChannelSubscribed(channelId, subscriptions) {
|
|
|
|
if (channelId === null) { return true }
|
|
|
|
const subExists = this.primaryProfile.subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === channelId
|
|
|
|
}) !== -1
|
|
|
|
|
|
|
|
const subDuplicateExists = subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === channelId
|
|
|
|
}) !== -1
|
|
|
|
return subExists || subDuplicateExists
|
|
|
|
},
|
|
|
|
|
2020-09-07 00:12:25 +02:00
|
|
|
...mapActions([
|
|
|
|
'updateProfile',
|
|
|
|
'updateShowProgressBar',
|
|
|
|
'updateHistory',
|
2022-02-06 20:31:27 +01:00
|
|
|
'addPlaylist',
|
2024-01-03 19:44:57 +01:00
|
|
|
'addVideo',
|
|
|
|
'updatePlaylist',
|
2020-09-07 00:12:25 +02:00
|
|
|
]),
|
|
|
|
|
|
|
|
...mapMutations([
|
|
|
|
'setProgressBarPercentage'
|
|
|
|
])
|
|
|
|
}
|
|
|
|
})
|