2020-09-07 00:12:25 +02:00
|
|
|
import Vue from 'vue'
|
|
|
|
import { mapActions, mapMutations } from 'vuex'
|
|
|
|
import FtCard from '../ft-card/ft-card.vue'
|
|
|
|
import FtButton from '../ft-button/ft-button.vue'
|
|
|
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
|
|
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
|
|
|
import FtPrompt from '../ft-prompt/ft-prompt.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
|
|
|
|
|
|
|
import fs from 'fs'
|
2021-01-21 20:19:25 +01:00
|
|
|
import { opmlToJSON } from 'opml-to-json'
|
2020-09-07 00:12:25 +02:00
|
|
|
import ytch from 'yt-channel-info'
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
// FIXME: Missing web logic branching
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
export default Vue.extend({
|
|
|
|
name: 'DataSettings',
|
|
|
|
components: {
|
|
|
|
'ft-card': FtCard,
|
|
|
|
'ft-button': FtButton,
|
|
|
|
'ft-toggle-switch': FtToggleSwitch,
|
|
|
|
'ft-flex-box': FtFlexBox,
|
|
|
|
'ft-prompt': FtPrompt
|
|
|
|
},
|
|
|
|
data: function () {
|
|
|
|
return {
|
|
|
|
showImportSubscriptionsPrompt: false,
|
|
|
|
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'
|
|
|
|
]
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
rememberHistory: function () {
|
|
|
|
return this.$store.getters.getRememberHistory
|
|
|
|
},
|
|
|
|
saveWatchedProgress: function () {
|
|
|
|
return this.$store.getters.getSaveWatchedProgress
|
|
|
|
},
|
|
|
|
backendPreference: function () {
|
|
|
|
return this.$store.getters.getBackendPreference
|
|
|
|
},
|
|
|
|
backendFallback: function () {
|
|
|
|
return this.$store.getters.getBackendFallback
|
|
|
|
},
|
2021-07-03 03:55:56 +02:00
|
|
|
currentInvidiousInstance: function () {
|
|
|
|
return this.$store.getters.getCurrentInvidiousInstance
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
profileList: function () {
|
|
|
|
return this.$store.getters.getProfileList
|
|
|
|
},
|
2022-02-06 20:31:27 +01:00
|
|
|
allPlaylists: function () {
|
|
|
|
return this.$store.getters.getAllPlaylists
|
|
|
|
},
|
2020-09-07 00:12:25 +02:00
|
|
|
importSubscriptionsPromptNames: function () {
|
|
|
|
const importFreeTube = this.$t('Settings.Data Settings.Import FreeTube')
|
|
|
|
const importYouTube = this.$t('Settings.Data Settings.Import YouTube')
|
|
|
|
const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe')
|
|
|
|
return [
|
|
|
|
`${importFreeTube} (.db)`,
|
2021-08-25 09:47:21 +02:00
|
|
|
`${importYouTube} (.csv)`,
|
2020-11-01 22:23:06 +01:00
|
|
|
`${importYouTube} (.json)`,
|
2020-09-07 00:12:25 +02:00
|
|
|
`${importYouTube} (.opml)`,
|
|
|
|
`${importNewPipe} (.json)`
|
|
|
|
]
|
|
|
|
},
|
|
|
|
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)`
|
|
|
|
]
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2021-01-14 19:51:33 +01:00
|
|
|
openProfileSettings: function () {
|
|
|
|
this.$router.push({
|
|
|
|
path: '/settings/profile/'
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-09-07 00:12:25 +02:00
|
|
|
importSubscriptions: function (option) {
|
|
|
|
this.showImportSubscriptionsPrompt = false
|
|
|
|
|
|
|
|
if (option === null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (option) {
|
|
|
|
case 'freetube':
|
|
|
|
this.importFreeTubeSubscriptions()
|
|
|
|
break
|
2021-08-25 09:47:21 +02:00
|
|
|
case 'youtubenew':
|
|
|
|
this.importCsvYouTubeSubscriptions()
|
|
|
|
break
|
2020-09-07 00:12:25 +02:00
|
|
|
case 'youtube':
|
|
|
|
this.importYouTubeSubscriptions()
|
|
|
|
break
|
2020-11-01 22:23:06 +01:00
|
|
|
case 'youtubeold':
|
|
|
|
this.importOpmlYouTubeSubscriptions()
|
|
|
|
break
|
2020-09-07 00:12:25 +02:00
|
|
|
case 'newpipe':
|
|
|
|
this.importNewPipeSubscriptions()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2020-10-08 23:35:36 +02:00
|
|
|
handleFreetubeImportFile: function (filePath) {
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let textDecode = new TextDecoder('utf-8').decode(data)
|
|
|
|
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:
|
|
|
|
textDecode = await this.convertOldFreeTubeFormatToNew(textDecode)
|
|
|
|
}
|
|
|
|
|
2020-10-09 03:19:29 +02:00
|
|
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
|
|
|
2020-10-08 23:35:36 +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.
|
|
|
|
|
|
|
|
const requiredKeys = [
|
|
|
|
'_id',
|
|
|
|
'name',
|
|
|
|
'bgColor',
|
|
|
|
'textColor',
|
|
|
|
'subscriptions'
|
|
|
|
]
|
|
|
|
|
|
|
|
const profileObject = {}
|
|
|
|
Object.keys(profileData).forEach((key) => {
|
|
|
|
if (!requiredKeys.includes(key)) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unknown data key')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${key}`
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
profileObject[key] = profileData[key]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (Object.keys(profileObject).length < requiredKeys.length) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Profile object has insufficient data, skipping item')
|
|
|
|
this.showToast({
|
|
|
|
message: message
|
|
|
|
})
|
|
|
|
} else {
|
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
|
|
|
if (profileObject.name === 'All Channels' || profileObject._id === MAIN_PROFILE_ID) {
|
2020-10-08 23:35:36 +02:00
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
2020-10-09 03:19:29 +02:00
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = primaryProfile.subscriptions.findIndex((x) => {
|
|
|
|
return x.name === sub.name
|
|
|
|
})
|
|
|
|
|
|
|
|
return profileIndex === index
|
|
|
|
})
|
2020-10-08 23:35:36 +02:00
|
|
|
this.updateProfile(primaryProfile)
|
|
|
|
} else {
|
2020-10-09 03:19:29 +02:00
|
|
|
const existingProfileIndex = this.profileList.findIndex((profile) => {
|
|
|
|
return profile.name.includes(profileObject.name)
|
|
|
|
})
|
|
|
|
|
|
|
|
if (existingProfileIndex !== -1) {
|
|
|
|
const existingProfile = JSON.parse(JSON.stringify(this.profileList[existingProfileIndex]))
|
|
|
|
existingProfile.subscriptions = existingProfile.subscriptions.concat(profileObject.subscriptions)
|
|
|
|
existingProfile.subscriptions = existingProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = existingProfile.subscriptions.findIndex((x) => {
|
|
|
|
return x.name === sub.name
|
|
|
|
})
|
|
|
|
|
|
|
|
return profileIndex === index
|
|
|
|
})
|
|
|
|
this.updateProfile(existingProfile)
|
|
|
|
} else {
|
|
|
|
this.updateProfile(profileObject)
|
|
|
|
}
|
|
|
|
|
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => {
|
|
|
|
const profileIndex = primaryProfile.subscriptions.findIndex((x) => {
|
|
|
|
return x.name === sub.name
|
|
|
|
})
|
|
|
|
|
|
|
|
return profileIndex === index
|
|
|
|
})
|
|
|
|
this.updateProfile(primaryProfile)
|
2020-10-08 23:35:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All subscriptions and profiles have been successfully imported')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
importFreeTubeSubscriptions: async function () {
|
2020-09-07 00:12:25 +02:00
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
this.handleFreetubeImportFile(filePath)
|
2020-09-07 00:12:25 +02:00
|
|
|
},
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
handleYoutubeCsvImportFile: function(filePath) { // first row = header, last row = empty
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const textDecode = new TextDecoder('utf-8').decode(data)
|
|
|
|
const youtubeSubscriptions = textDecode.split('\n')
|
|
|
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
|
|
const subscriptions = []
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
|
|
|
})
|
|
|
|
|
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
let count = 0
|
|
|
|
for (let i = 1; i < (youtubeSubscriptions.length - 1); i++) {
|
|
|
|
const channelId = youtubeSubscriptions[i].split(',')[0]
|
|
|
|
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === channelId
|
|
|
|
})
|
|
|
|
if (subExists === -1) {
|
|
|
|
let channelInfo
|
|
|
|
if (this.backendPreference === 'invidious') { // only needed for thumbnail
|
|
|
|
channelInfo = await this.getChannelInfoInvidious(channelId)
|
|
|
|
} else {
|
|
|
|
channelInfo = await this.getChannelInfoLocal(channelId)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof channelInfo.author !== 'undefined') {
|
|
|
|
const subscription = {
|
|
|
|
id: channelId,
|
|
|
|
name: channelInfo.author,
|
|
|
|
thumbnail: channelInfo.authorThumbnails[1].url
|
|
|
|
}
|
|
|
|
subscriptions.push(subscription)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
count++
|
|
|
|
|
|
|
|
const progressPercentage = (count / (youtubeSubscriptions.length - 1)) * 100
|
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
|
|
|
if (count + 1 === (youtubeSubscriptions.length - 1)) {
|
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(primaryProfile)
|
|
|
|
|
|
|
|
if (subscriptions.length < count + 2) {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported')
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-11-01 22:23:06 +01:00
|
|
|
handleYoutubeImportFile: function (filePath) {
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let textDecode = new TextDecoder('utf-8').decode(data)
|
|
|
|
textDecode = JSON.parse(textDecode)
|
|
|
|
|
|
|
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
|
|
const subscriptions = []
|
|
|
|
|
2021-04-21 04:43:40 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
|
|
|
})
|
|
|
|
|
2020-11-01 22:23:06 +01:00
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
textDecode.forEach((channel) => {
|
|
|
|
const snippet = channel.snippet
|
|
|
|
|
|
|
|
if (typeof snippet === 'undefined') {
|
2021-04-21 04:43:40 +02:00
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
this.showToast({
|
|
|
|
message: message
|
|
|
|
})
|
|
|
|
|
2020-11-01 22:23:06 +01:00
|
|
|
throw new Error('Unable to find channel data')
|
|
|
|
}
|
|
|
|
|
|
|
|
const subscription = {
|
|
|
|
id: snippet.resourceId.channelId,
|
|
|
|
name: snippet.title,
|
|
|
|
thumbnail: snippet.thumbnails.default.url
|
|
|
|
}
|
|
|
|
|
|
|
|
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === subscription.id || sub.name === subscription.name
|
|
|
|
})
|
|
|
|
|
2020-12-30 20:46:48 +01:00
|
|
|
const subDuplicateExists = subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === subscription.id || sub.name === subscription.name
|
|
|
|
})
|
|
|
|
|
|
|
|
if (subExists === -1 && subDuplicateExists === -1) {
|
2020-11-01 22:23:06 +01:00
|
|
|
subscriptions.push(subscription)
|
|
|
|
}
|
|
|
|
|
|
|
|
count++
|
|
|
|
|
|
|
|
const progressPercentage = (count / textDecode.length) * 100
|
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
|
|
|
|
|
|
|
if (count === textDecode.length) {
|
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(primaryProfile)
|
|
|
|
|
|
|
|
if (subscriptions.length < count) {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported')
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
importCsvYouTubeSubscriptions: async function () {
|
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['csv']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
this.handleYoutubeCsvImportFile(filePath)
|
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
importYouTubeSubscriptions: async function () {
|
2020-11-01 22:23:06 +01:00
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['json']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
this.handleYoutubeImportFile(filePath)
|
2020-11-01 22:23:06 +01:00
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
importOpmlYouTubeSubscriptions: async function () {
|
2020-09-07 00:12:25 +02:00
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
2020-09-29 01:53:59 +02:00
|
|
|
extensions: ['opml', 'xml']
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePaths[0]
|
2020-09-12 22:59:49 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
opmlToJSON(data).then((json) => {
|
|
|
|
let feedData = json.children[0].children
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
if (typeof feedData === 'undefined') {
|
|
|
|
if (json.title.includes('gPodder')) {
|
|
|
|
feedData = json.children
|
|
|
|
} else {
|
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
this.showToast({
|
|
|
|
message: message
|
|
|
|
})
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
|
|
const subscriptions = []
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
|
|
|
})
|
|
|
|
|
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
|
2020-09-12 22:59:49 +02:00
|
|
|
let count = 0
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
feedData.forEach(async (channel, index) => {
|
|
|
|
const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
|
2021-08-25 09:47:21 +02:00
|
|
|
const subExists = 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)
|
2020-09-12 22:59:49 +02:00
|
|
|
}
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
if (typeof channelInfo.author !== 'undefined') {
|
|
|
|
const subscription = {
|
|
|
|
id: channelId,
|
|
|
|
name: channelInfo.author,
|
|
|
|
thumbnail: channelInfo.authorThumbnails[1].url
|
|
|
|
}
|
2020-09-26 03:56:54 +02:00
|
|
|
subscriptions.push(subscription)
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
|
2020-09-12 22:59:49 +02:00
|
|
|
count++
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const progressPercentage = (count / feedData.length) * 100
|
2020-09-07 00:12:25 +02:00
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
if (count === feedData.length) {
|
2020-09-07 00:12:25 +02:00
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(primaryProfile)
|
|
|
|
|
2020-09-12 22:59:49 +02:00
|
|
|
if (subscriptions.length < count) {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported')
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-09-07 00:12:25 +02:00
|
|
|
this.updateShowProgressBar(false)
|
|
|
|
}
|
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
}).catch((err) => {
|
|
|
|
console.log(err)
|
|
|
|
console.log('error reading')
|
|
|
|
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
importNewPipeSubscriptions: async function () {
|
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['json']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const newPipeData = JSON.parse(data)
|
|
|
|
|
|
|
|
if (typeof newPipeData.subscriptions === 'undefined') {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
|
|
})
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-23 17:42:04 +02:00
|
|
|
const newPipeSubscriptions = newPipeData.subscriptions.filter((channel, index) => {
|
|
|
|
return channel.service_id === 0
|
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
|
|
|
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
|
|
const subscriptions = []
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
|
|
|
})
|
|
|
|
|
|
|
|
this.updateShowProgressBar(true)
|
|
|
|
this.setProgressBarPercentage(0)
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
newPipeSubscriptions.forEach(async (channel, index) => {
|
|
|
|
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
|
2021-08-25 09:47:21 +02:00
|
|
|
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
|
|
|
return sub.id === channelId
|
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-08-25 09:47:21 +02: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
|
|
|
subscriptions.push(subscription)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
count++
|
|
|
|
|
|
|
|
const progressPercentage = (count / newPipeSubscriptions.length) * 100
|
|
|
|
this.setProgressBarPercentage(progressPercentage)
|
|
|
|
|
|
|
|
if (count === newPipeSubscriptions.length) {
|
|
|
|
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
|
|
|
|
this.updateProfile(primaryProfile)
|
|
|
|
|
|
|
|
if (subscriptions.length < count) {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.One or more subscriptions were unable to be imported')
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All subscriptions have been successfully imported')
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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 () {
|
|
|
|
await this.compactProfiles()
|
2021-05-22 01:49:48 +02:00
|
|
|
const userData = await this.getUserDataPath()
|
2020-09-07 00:12:25 +02:00
|
|
|
const subscriptionsDb = `${userData}/profiles.db`
|
2021-08-25 09:47:21 +02:00
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'freetube-subscriptions-' + date + '.db'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePath
|
|
|
|
|
|
|
|
fs.readFile(subscriptionsDb, (readErr, data) => {
|
|
|
|
if (readErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${readErr}`
|
|
|
|
})
|
2020-09-07 00:12:25 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.writeFile(filePath, data, (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `${message}: ${writeErr}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
exportYouTubeSubscriptions: async function () {
|
2021-08-25 09:47:21 +02:00
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + date + '.json'
|
2020-11-01 22:23:06 +01:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePath
|
2020-11-01 22:23:06 +01:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.writeFile(filePath, JSON.stringify(subscriptionsObject), (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
2020-11-01 22:23:06 +01:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `${message}: ${writeErr}`
|
2020-11-01 22:23:06 +01:00
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
2020-11-01 22:23:06 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
exportOpmlYouTubeSubscriptions: async function () {
|
2021-08-25 09:47:21 +02:00
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + date + '.opml'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['opml']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
let opmlData = '<opml version="1.1"><body><outline text="YouTube Subscriptions" title="YouTube Subscriptions">'
|
|
|
|
const endingOpmlString = '</outline></body></opml>'
|
|
|
|
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
this.profileList[0].subscriptions.forEach((channel) => {
|
|
|
|
const channelOpmlString = `<outline text="${channel.name}" title="${channel.name}" type="rss" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}"/>`
|
|
|
|
count++
|
|
|
|
opmlData += channelOpmlString
|
|
|
|
|
|
|
|
if (count === this.profileList[0].subscriptions.length) {
|
|
|
|
opmlData += endingOpmlString
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePath
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.writeFile(filePath, opmlData, (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `${message}: ${writeErr}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
exportCsvYouTubeSubscriptions: async function () {
|
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'youtube-subscriptions-' + date + '.csv'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
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}`
|
|
|
|
exportText += `${channel.id},${channelUrl},${channel.name}\n`
|
|
|
|
})
|
|
|
|
exportText += '\n'
|
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
const filePath = response.filePath
|
|
|
|
fs.writeFile(filePath, exportText, (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${writeErr}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-08-25 09:47:21 +02:00
|
|
|
exportNewPipeSubscriptions: async function () {
|
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'newpipe-subscriptions-' + date + '.json'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
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)
|
|
|
|
})
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const filePath = response.filePath
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.writeFile(filePath, JSON.stringify(newPipeObject), (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `${message}: ${writeErr}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
2021-05-22 01:49:48 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
checkForLegacySubscriptions: async function () {
|
|
|
|
let dbLocation = await this.getUserDataPath()
|
2020-10-08 23:35:36 +02:00
|
|
|
dbLocation = dbLocation + '/subscriptions.db'
|
|
|
|
this.handleFreetubeImportFile(dbLocation)
|
|
|
|
fs.unlink(dbLocation, (err) => {
|
|
|
|
if (err) {
|
|
|
|
console.log(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
importHistory: async function () {
|
2020-09-07 00:12:25 +02:00
|
|
|
const options = {
|
|
|
|
properties: ['openFile'],
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
2020-09-07 00:12:25 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
let textDecode = new TextDecoder('utf-8').decode(data)
|
|
|
|
textDecode = textDecode.split('\n')
|
|
|
|
textDecode.pop()
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +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.
|
|
|
|
const requiredKeys = [
|
|
|
|
'_id',
|
|
|
|
'author',
|
|
|
|
'authorId',
|
|
|
|
'description',
|
|
|
|
'isLive',
|
|
|
|
'lengthSeconds',
|
|
|
|
'paid',
|
|
|
|
'published',
|
|
|
|
'timeWatched',
|
|
|
|
'title',
|
|
|
|
'type',
|
|
|
|
'videoId',
|
|
|
|
'viewCount',
|
|
|
|
'watchProgress'
|
|
|
|
]
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const historyObject = {}
|
2020-09-07 00:12:25 +02:00
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
Object.keys(historyData).forEach((key) => {
|
|
|
|
if (!requiredKeys.includes(key)) {
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `Unknown data key: ${key}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
} else {
|
2021-05-22 01:49:48 +02:00
|
|
|
historyObject[key] = historyData[key]
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
if (Object.keys(historyObject).length < (requiredKeys.length - 2)) {
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.History object has insufficient data, skipping item')
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.updateHistory(historyObject)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All watched history has been successfully imported')
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2021-04-21 06:09:06 +02:00
|
|
|
exportHistory: async function () {
|
|
|
|
await this.compactHistory()
|
2021-05-22 01:49:48 +02:00
|
|
|
const userData = await this.getUserDataPath()
|
2020-09-07 00:12:25 +02:00
|
|
|
const historyDb = `${userData}/history.db`
|
2021-08-25 09:47:21 +02:00
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'freetube-history-' + date + '.db'
|
2020-09-07 00:12:25 +02:00
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePath
|
|
|
|
|
|
|
|
fs.readFile(historyDb, (readErr, data) => {
|
|
|
|
if (readErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${readErr}`
|
|
|
|
})
|
2020-09-07 00:12:25 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
fs.writeFile(filePath, data, (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
2021-05-22 01:49:48 +02:00
|
|
|
message: `${message}: ${writeErr}`
|
2020-09-07 00:12:25 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-05-22 01:49:48 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.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: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.showOpenDialog(options)
|
|
|
|
if (response.canceled || response.filePaths.length === 0) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePaths[0]
|
|
|
|
|
|
|
|
fs.readFile(filePath, async (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${err}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const playlists = JSON.parse(data)
|
|
|
|
|
|
|
|
playlists.forEach(async (playlistData) => {
|
|
|
|
// 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 requiredKeys = [
|
|
|
|
'playlistName',
|
|
|
|
'videos'
|
|
|
|
]
|
|
|
|
|
|
|
|
const optionalKeys = [
|
|
|
|
'_id',
|
|
|
|
'protected',
|
|
|
|
'removeOnWatched'
|
|
|
|
]
|
|
|
|
|
|
|
|
const requiredVideoKeys = [
|
|
|
|
'videoId',
|
|
|
|
'title',
|
|
|
|
'author',
|
|
|
|
'authorId',
|
|
|
|
'published',
|
|
|
|
'lengthSeconds',
|
|
|
|
'timeAdded',
|
|
|
|
'isLive',
|
|
|
|
'paid',
|
|
|
|
'type'
|
|
|
|
]
|
|
|
|
|
|
|
|
const playlistObject = {}
|
|
|
|
|
|
|
|
Object.keys(playlistData).forEach((key) => {
|
|
|
|
if (!requiredKeys.includes(key) && !optionalKeys.includes(key)) {
|
|
|
|
const message = `${this.$t('Settings.Data Settings.Unknown data key')}: ${key}`
|
|
|
|
this.showToast({
|
|
|
|
message: message
|
|
|
|
})
|
|
|
|
} else if (key === 'videos') {
|
|
|
|
const videoArray = []
|
|
|
|
playlistData.videos.forEach((video) => {
|
|
|
|
let hasAllKeys = true
|
2022-02-07 03:00:52 +01:00
|
|
|
requiredVideoKeys.forEach((videoKey) => {
|
2022-02-06 22:48:24 +01:00
|
|
|
if (!Object.keys(video).includes(videoKey)) {
|
2022-02-06 20:31:27 +01:00
|
|
|
hasAllKeys = false
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (hasAllKeys) {
|
|
|
|
videoArray.push(video)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
playlistObject[key] = videoArray
|
|
|
|
} else {
|
|
|
|
playlistObject[key] = playlistData[key]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const objectKeys = Object.keys(playlistObject)
|
|
|
|
|
|
|
|
if ((objectKeys.length < requiredKeys.length) || playlistObject.videos.length === 0) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Playlist insufficient data').replace('$', playlistData.playlistName)
|
|
|
|
this.showToast({
|
|
|
|
message: message
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
const existingPlaylist = this.allPlaylists.find((playlist) => {
|
|
|
|
return playlist.playlistName === playlistObject.playlistName
|
|
|
|
})
|
|
|
|
|
|
|
|
if (existingPlaylist !== undefined) {
|
|
|
|
playlistObject.videos.forEach((video) => {
|
|
|
|
const existingVideo = existingPlaylist.videos.find((x) => {
|
|
|
|
return x.videoId === video.videoId
|
|
|
|
})
|
|
|
|
|
|
|
|
if (existingVideo === undefined) {
|
|
|
|
const payload = {
|
|
|
|
playlistName: existingPlaylist.playlistName,
|
|
|
|
videoData: video
|
|
|
|
}
|
|
|
|
|
|
|
|
this.addVideo(payload)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.addPlaylist(playlistObject)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All playlists has been successfully imported')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
exportPlaylists: async function () {
|
|
|
|
const date = new Date().toISOString().split('T')[0]
|
|
|
|
const exportFileName = 'freetube-playlists-' + date + '.db'
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
defaultPath: exportFileName,
|
|
|
|
filters: [
|
|
|
|
{
|
|
|
|
name: 'Database File',
|
|
|
|
extensions: ['db']
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.showSaveDialog(options)
|
|
|
|
if (response.canceled || response.filePath === '') {
|
|
|
|
// User canceled the save dialog
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const filePath = response.filePath
|
|
|
|
|
|
|
|
fs.writeFile(filePath, JSON.stringify(this.allPlaylists), (writeErr) => {
|
|
|
|
if (writeErr) {
|
|
|
|
const message = this.$t('Settings.Data Settings.Unable to write file')
|
|
|
|
this.showToast({
|
|
|
|
message: `${message}: ${writeErr}`
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Settings.Data Settings.All playlists has been successfully exported')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-09-12 10:00:18 +02:00
|
|
|
async convertOldFreeTubeFormatToNew(oldData) {
|
|
|
|
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
|
|
|
|
const randomBgColor = await this.getRandomColor()
|
|
|
|
const contrastyTextColor = await this.calculateColorLuminance(randomBgColor)
|
|
|
|
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
|
|
|
|
},
|
|
|
|
|
2020-09-07 00:12:25 +02:00
|
|
|
getChannelInfoInvidious: function (channelId) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const subscriptionsPayload = {
|
|
|
|
resource: 'channels',
|
|
|
|
id: channelId,
|
|
|
|
params: {}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.invidiousAPICall(subscriptionsPayload).then((response) => {
|
|
|
|
resolve(response)
|
|
|
|
}).catch((err) => {
|
|
|
|
console.log(err)
|
|
|
|
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
|
|
|
this.showToast({
|
2020-09-16 14:58:39 +02:00
|
|
|
message: `${errorMessage}: ${err.responseJSON.error}`,
|
2020-09-07 00:12:25 +02:00
|
|
|
time: 10000,
|
|
|
|
action: () => {
|
2020-09-16 14:58:39 +02:00
|
|
|
navigator.clipboard.writeText(err.responseJSON.error)
|
2020-09-07 00:12:25 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-09-16 14:58:39 +02:00
|
|
|
if (this.backendFallback && this.backendPreference === 'invidious') {
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Falling back to the local API')
|
|
|
|
})
|
|
|
|
resolve(this.getChannelInfoLocal(channelId))
|
|
|
|
} else {
|
|
|
|
resolve([])
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
getChannelInfoLocal: function (channelId) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
ytch.getChannelInfo(channelId, 'latest').then(async (response) => {
|
|
|
|
resolve(response)
|
|
|
|
}).catch((err) => {
|
|
|
|
console.log(err)
|
|
|
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
|
|
|
this.showToast({
|
|
|
|
message: `${errorMessage}: ${err}`,
|
|
|
|
time: 10000,
|
|
|
|
action: () => {
|
|
|
|
navigator.clipboard.writeText(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-09-16 14:58:39 +02:00
|
|
|
if (this.backendFallback && this.backendPreference === 'local') {
|
2020-09-07 00:12:25 +02:00
|
|
|
this.showToast({
|
|
|
|
message: this.$t('Falling back to the Invidious API')
|
|
|
|
})
|
|
|
|
resolve(this.getChannelInfoInvidious(channelId))
|
|
|
|
} else {
|
|
|
|
resolve([])
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
...mapActions([
|
|
|
|
'invidiousAPICall',
|
|
|
|
'updateProfile',
|
2021-04-21 06:09:06 +02:00
|
|
|
'compactProfiles',
|
2020-09-07 00:12:25 +02:00
|
|
|
'updateShowProgressBar',
|
|
|
|
'updateHistory',
|
2021-04-21 06:09:06 +02:00
|
|
|
'compactHistory',
|
2020-09-12 10:00:18 +02:00
|
|
|
'showToast',
|
|
|
|
'getRandomColor',
|
2021-05-22 01:49:48 +02:00
|
|
|
'calculateColorLuminance',
|
|
|
|
'showOpenDialog',
|
|
|
|
'showSaveDialog',
|
2022-02-06 20:31:27 +01:00
|
|
|
'getUserDataPath',
|
|
|
|
'addPlaylist',
|
|
|
|
'addVideo'
|
2020-09-07 00:12:25 +02:00
|
|
|
]),
|
|
|
|
|
|
|
|
...mapMutations([
|
|
|
|
'setProgressBarPercentage'
|
|
|
|
])
|
|
|
|
}
|
|
|
|
})
|