Improve Importing Subscriptions (#2604)
* improve import * fix merge conflicts * dont add duplicate subscriptions, remove redundant "uniqueId" * Update src/renderer/components/data-settings/data-settings.js Co-authored-by: PikachuEXE <pikachuexe@gmail.com> * fix unexpected errors dont show toast when no errors. dont error when already subscribed * remove check for legacy subscriptions * rename method * deduplicate importing code * remove unused code Co-authored-by: PikachuEXE <pikachuexe@gmail.com>
This commit is contained in:
parent
2154255ec3
commit
41fee01217
|
@ -59,18 +59,6 @@ export default Vue.extend({
|
||||||
allPlaylists: function () {
|
allPlaylists: function () {
|
||||||
return this.$store.getters.getAllPlaylists
|
return this.$store.getters.getAllPlaylists
|
||||||
},
|
},
|
||||||
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)`,
|
|
||||||
`${importYouTube} (.csv)`,
|
|
||||||
`${importYouTube} (.json)`,
|
|
||||||
`${importYouTube} (.opml)`,
|
|
||||||
`${importNewPipe} (.json)`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
exportSubscriptionsPromptNames: function () {
|
exportSubscriptionsPromptNames: function () {
|
||||||
const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube')
|
const exportFreeTube = this.$t('Settings.Data Settings.Export FreeTube')
|
||||||
const exportYouTube = this.$t('Settings.Data Settings.Export YouTube')
|
const exportYouTube = this.$t('Settings.Data Settings.Export YouTube')
|
||||||
|
@ -85,6 +73,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
usingElectron: function () {
|
usingElectron: function () {
|
||||||
return process.env.IS_ELECTRON
|
return process.env.IS_ELECTRON
|
||||||
|
},
|
||||||
|
primaryProfile: function () {
|
||||||
|
return JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -94,33 +85,21 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
importSubscriptions: function (option) {
|
importSubscriptions: async function () {
|
||||||
this.showImportSubscriptionsPrompt = false
|
const options = {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
|
extensions: ['db', 'csv', 'json', 'opml', 'xml']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if (option === null) {
|
const response = await this.showOpenDialog(options)
|
||||||
|
if (response.canceled || response.filePaths?.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (option) {
|
|
||||||
case 'freetube':
|
|
||||||
this.importFreeTubeSubscriptions()
|
|
||||||
break
|
|
||||||
case 'youtubenew':
|
|
||||||
this.importCsvYouTubeSubscriptions()
|
|
||||||
break
|
|
||||||
case 'youtube':
|
|
||||||
this.importYouTubeSubscriptions()
|
|
||||||
break
|
|
||||||
case 'youtubeold':
|
|
||||||
this.importOpmlYouTubeSubscriptions()
|
|
||||||
break
|
|
||||||
case 'newpipe':
|
|
||||||
this.importNewPipeSubscriptions()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFreetubeImportFile: async function (response) {
|
|
||||||
let textDecode
|
let textDecode
|
||||||
try {
|
try {
|
||||||
textDecode = await this.readFileFromDialog({ response })
|
textDecode = await this.readFileFromDialog({ response })
|
||||||
|
@ -131,6 +110,25 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
importFreeTubeSubscriptions: async function (textDecode) {
|
||||||
textDecode = textDecode.split('\n')
|
textDecode = textDecode.split('\n')
|
||||||
textDecode.pop()
|
textDecode.pop()
|
||||||
textDecode = textDecode.map(data => JSON.parse(data))
|
textDecode = textDecode.map(data => JSON.parse(data))
|
||||||
|
@ -141,8 +139,6 @@ export default Vue.extend({
|
||||||
textDecode = await this.convertOldFreeTubeFormatToNew(textDecode)
|
textDecode = await this.convertOldFreeTubeFormatToNew(textDecode)
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
||||||
|
|
||||||
textDecode.forEach((profileData) => {
|
textDecode.forEach((profileData) => {
|
||||||
// We would technically already be done by the time the data is parsed,
|
// 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
|
// however we want to limit the possibility of malicious data being sent
|
||||||
|
@ -175,15 +171,15 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (profileObject.name === 'All Channels' || profileObject._id === MAIN_PROFILE_ID) {
|
if (profileObject.name === 'All Channels' || profileObject._id === MAIN_PROFILE_ID) {
|
||||||
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
||||||
primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => {
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => {
|
||||||
const profileIndex = primaryProfile.subscriptions.findIndex((x) => {
|
const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => {
|
||||||
return x.name === sub.name
|
return x.name === sub.name
|
||||||
})
|
})
|
||||||
|
|
||||||
return profileIndex === index
|
return profileIndex === index
|
||||||
})
|
})
|
||||||
this.updateProfile(primaryProfile)
|
this.updateProfile(this.primaryProfile)
|
||||||
} else {
|
} else {
|
||||||
const existingProfileIndex = this.profileList.findIndex((profile) => {
|
const existingProfileIndex = this.profileList.findIndex((profile) => {
|
||||||
return profile.name.includes(profileObject.name)
|
return profile.name.includes(profileObject.name)
|
||||||
|
@ -204,15 +200,15 @@ export default Vue.extend({
|
||||||
this.updateProfile(profileObject)
|
this.updateProfile(profileObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(profileObject.subscriptions)
|
||||||
primaryProfile.subscriptions = primaryProfile.subscriptions.filter((sub, index) => {
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.filter((sub, index) => {
|
||||||
const profileIndex = primaryProfile.subscriptions.findIndex((x) => {
|
const profileIndex = this.primaryProfile.subscriptions.findIndex((x) => {
|
||||||
return x.name === sub.name
|
return x.name === sub.name
|
||||||
})
|
})
|
||||||
|
|
||||||
return profileIndex === index
|
return profileIndex === index
|
||||||
})
|
})
|
||||||
this.updateProfile(primaryProfile)
|
this.updateProfile(this.primaryProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -222,41 +218,12 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
importFreeTubeSubscriptions: async function () {
|
importCsvYouTubeSubscriptions: async function(textDecode) { // first row = header, last row = empty
|
||||||
const options = {
|
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Database File',
|
|
||||||
extensions: ['db']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.showOpenDialog(options)
|
|
||||||
if (response.canceled || response.filePaths?.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleFreetubeImportFile(response)
|
|
||||||
},
|
|
||||||
|
|
||||||
handleYoutubeCsvImportFile: async function(response) { // first row = header, last row = empty
|
|
||||||
let textDecode
|
|
||||||
try {
|
|
||||||
textDecode = await this.readFileFromDialog({ response })
|
|
||||||
} catch (err) {
|
|
||||||
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
||||||
this.showToast({
|
|
||||||
message: `${message}: ${err}`
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const youtubeSubscriptions = textDecode.split('\n').filter(sub => {
|
const youtubeSubscriptions = textDecode.split('\n').filter(sub => {
|
||||||
return sub !== ''
|
return sub !== ''
|
||||||
})
|
})
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
||||||
const subscriptions = []
|
const subscriptions = []
|
||||||
|
const errorList = []
|
||||||
|
|
||||||
this.showToast({
|
this.showToast({
|
||||||
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
||||||
|
@ -265,195 +232,121 @@ export default Vue.extend({
|
||||||
this.updateShowProgressBar(true)
|
this.updateShowProgressBar(true)
|
||||||
this.setProgressBarPercentage(0)
|
this.setProgressBarPercentage(0)
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let i = 1; i < (youtubeSubscriptions.length - 1); i++) {
|
|
||||||
const channelId = youtubeSubscriptions[i].split(',')[0]
|
const ytsubs = youtubeSubscriptions.slice(1).map(yt => {
|
||||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
const splitCSVRegex = /(?:,|\n|^)("(?:(?:"")*[^"]*)*"|[^",\n]*|(?:\n|$))/g
|
||||||
return sub.id === channelId
|
return [...yt.matchAll(splitCSVRegex)].map(s => {
|
||||||
|
let newVal = s[1]
|
||||||
|
if (newVal.startsWith('"')) {
|
||||||
|
newVal = newVal.substring(1, newVal.length - 2).replace('""', '"')
|
||||||
|
}
|
||||||
|
return newVal
|
||||||
})
|
})
|
||||||
if (subExists === -1) {
|
}).filter(channel => {
|
||||||
let channelInfo
|
return channel.length > 0
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleYoutubeImportFile: async function (response) {
|
|
||||||
let textDecode
|
|
||||||
try {
|
|
||||||
textDecode = await this.readFileFromDialog({ response })
|
|
||||||
} catch (err) {
|
|
||||||
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
||||||
this.showToast({
|
|
||||||
message: `${message}: ${err}`
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
textDecode = JSON.parse(textDecode)
|
|
||||||
|
|
||||||
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')
|
|
||||||
})
|
})
|
||||||
|
new Promise((resolve) => {
|
||||||
this.updateShowProgressBar(true)
|
let finishCount = 0
|
||||||
this.setProgressBarPercentage(0)
|
ytsubs.forEach(async (yt) => {
|
||||||
|
const { subscription, result } = await this.subscribeToChannel({
|
||||||
let count = 0
|
channelId: yt[0],
|
||||||
|
subscriptions: subscriptions,
|
||||||
textDecode.forEach((channel) => {
|
channelName: yt[2],
|
||||||
const snippet = channel.snippet
|
count: count++,
|
||||||
|
total: ytsubs.length
|
||||||
if (typeof snippet === 'undefined') {
|
|
||||||
const message = this.$t('Settings.Data Settings.Invalid subscriptions file')
|
|
||||||
this.showToast({
|
|
||||||
message: message
|
|
||||||
})
|
})
|
||||||
|
if (result === 1) {
|
||||||
throw new Error('Unable to find channel data')
|
subscriptions.push(subscription)
|
||||||
}
|
} else if (result === -1) {
|
||||||
|
errorList.push(yt)
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
const subDuplicateExists = subscriptions.findIndex((sub) => {
|
|
||||||
return sub.id === subscription.id || sub.name === subscription.name
|
|
||||||
})
|
|
||||||
|
|
||||||
if (subExists === -1 && subDuplicateExists === -1) {
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
finishCount++
|
||||||
this.updateShowProgressBar(false)
|
if (finishCount === ytsubs.length) {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).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]}.`)
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}).finally(_ => {
|
||||||
|
this.updateShowProgressBar(false)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
importCsvYouTubeSubscriptions: async function () {
|
importYouTubeSubscriptions: async function (textDecode) {
|
||||||
const options = {
|
const subscriptions = []
|
||||||
properties: ['openFile'],
|
const errorList = []
|
||||||
filters: [
|
|
||||||
{
|
this.showToast({
|
||||||
name: 'Database File',
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
||||||
extensions: ['csv']
|
})
|
||||||
|
|
||||||
|
this.updateShowProgressBar(true)
|
||||||
|
this.setProgressBarPercentage(0)
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
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')
|
||||||
|
this.showToast({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
throw new Error('Unable to find channel data')
|
||||||
}
|
}
|
||||||
]
|
const { subscription, result } = await this.subscribeToChannel({
|
||||||
}
|
channelId: snippet.resourceId.channelId,
|
||||||
const response = await this.showOpenDialog(options)
|
subscriptions: subscriptions,
|
||||||
if (response.canceled || response.filePaths?.length === 0) {
|
channelName: snippet.title,
|
||||||
return
|
thumbnail: snippet.thumbnails.default.url,
|
||||||
}
|
count: count++,
|
||||||
|
total: textDecode.length
|
||||||
this.handleYoutubeCsvImportFile(response)
|
})
|
||||||
},
|
if (result === 1) {
|
||||||
|
subscriptions.push(subscription)
|
||||||
importYouTubeSubscriptions: async function () {
|
} else if (result === -1) {
|
||||||
const options = {
|
errorList.push([snippet.resourceId.channelId, `https://www.youtube.com/channel/${snippet.resourceId.channelId}`, snippet.title])
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Database File',
|
|
||||||
extensions: ['json']
|
|
||||||
}
|
}
|
||||||
]
|
finishCount++
|
||||||
}
|
if (finishCount === textDecode.length) {
|
||||||
|
resolve(true)
|
||||||
const response = await this.showOpenDialog(options)
|
|
||||||
if (response.canceled || response.filePaths?.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleYoutubeImportFile(response)
|
|
||||||
},
|
|
||||||
|
|
||||||
importOpmlYouTubeSubscriptions: async function () {
|
|
||||||
const options = {
|
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Database File',
|
|
||||||
extensions: ['opml', 'xml']
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.showOpenDialog(options)
|
|
||||||
if (response.canceled || response.filePaths?.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = await this.readFileFromDialog({ response })
|
|
||||||
} catch (err) {
|
|
||||||
const message = this.$t('Settings.Data Settings.Unable to read file')
|
|
||||||
this.showToast({
|
|
||||||
message: `${message}: ${err}`
|
|
||||||
})
|
})
|
||||||
return
|
}).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]}.`)
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).finally(_ => {
|
||||||
|
this.updateShowProgressBar(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
importOpmlYouTubeSubscriptions: async function (data) {
|
||||||
let json
|
let json
|
||||||
try {
|
try {
|
||||||
json = await opmlToJSON(data)
|
json = await opmlToJSON(data)
|
||||||
|
@ -476,12 +369,10 @@ export default Vue.extend({
|
||||||
this.showToast({
|
this.showToast({
|
||||||
message: message
|
message: message
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
||||||
const subscriptions = []
|
const subscriptions = []
|
||||||
|
|
||||||
this.showToast({
|
this.showToast({
|
||||||
|
@ -495,7 +386,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
feedData.forEach(async (channel, index) => {
|
feedData.forEach(async (channel, index) => {
|
||||||
const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
|
const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
|
||||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
const subExists = this.primaryProfile.subscriptions.findIndex((sub) => {
|
||||||
return sub.id === channelId
|
return sub.id === channelId
|
||||||
})
|
})
|
||||||
if (subExists === -1) {
|
if (subExists === -1) {
|
||||||
|
@ -522,8 +413,8 @@ export default Vue.extend({
|
||||||
this.setProgressBarPercentage(progressPercentage)
|
this.setProgressBarPercentage(progressPercentage)
|
||||||
|
|
||||||
if (count === feedData.length) {
|
if (count === feedData.length) {
|
||||||
primaryProfile.subscriptions = primaryProfile.subscriptions.concat(subscriptions)
|
this.primaryProfile.subscriptions = this.primaryProfile.subscriptions.concat(subscriptions)
|
||||||
this.updateProfile(primaryProfile)
|
this.updateProfile(this.primaryProfile)
|
||||||
|
|
||||||
if (subscriptions.length < count) {
|
if (subscriptions.length < count) {
|
||||||
this.showToast({
|
this.showToast({
|
||||||
|
@ -541,35 +432,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
importNewPipeSubscriptions: async function () {
|
importNewPipeSubscriptions: async function (newPipeData) {
|
||||||
const options = {
|
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Database File',
|
|
||||||
extensions: ['json']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.showOpenDialog(options)
|
|
||||||
if (response.canceled || response.filePaths?.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = await this.readFileFromDialog({ response })
|
|
||||||
} catch (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') {
|
if (typeof newPipeData.subscriptions === 'undefined') {
|
||||||
this.showToast({
|
this.showToast({
|
||||||
message: this.$t('Settings.Data Settings.Invalid subscriptions file')
|
message: this.$t('Settings.Data Settings.Invalid subscriptions file')
|
||||||
|
@ -582,8 +445,8 @@ export default Vue.extend({
|
||||||
return channel.service_id === 0
|
return channel.service_id === 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
|
||||||
const subscriptions = []
|
const subscriptions = []
|
||||||
|
const errorList = []
|
||||||
|
|
||||||
this.showToast({
|
this.showToast({
|
||||||
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
message: this.$t('Settings.Data Settings.This might take a while, please wait')
|
||||||
|
@ -594,51 +457,45 @@ export default Vue.extend({
|
||||||
|
|
||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
newPipeSubscriptions.forEach(async (channel, index) => {
|
new Promise((resolve) => {
|
||||||
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
|
let finishCount = 0
|
||||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
newPipeSubscriptions.forEach(async (channel, index) => {
|
||||||
return sub.id === channelId
|
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
|
||||||
})
|
const { subscription, result } = await this.subscribeToChannel({
|
||||||
|
channelId: channelId,
|
||||||
if (subExists === -1) {
|
subscriptions: subscriptions,
|
||||||
let channelInfo
|
channelName: channel.name,
|
||||||
if (this.backendPreference === 'invidious') {
|
count: count++,
|
||||||
channelInfo = await this.getChannelInfoInvidious(channelId)
|
total: newPipeSubscriptions.length
|
||||||
} else {
|
})
|
||||||
channelInfo = await this.getChannelInfoLocal(channelId)
|
if (result === 1) {
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof channelInfo.author !== 'undefined') {
|
|
||||||
const subscription = {
|
|
||||||
id: channelId,
|
|
||||||
name: channelInfo.author,
|
|
||||||
thumbnail: channelInfo.authorThumbnails[1].url
|
|
||||||
}
|
|
||||||
subscriptions.push(subscription)
|
subscriptions.push(subscription)
|
||||||
}
|
}
|
||||||
}
|
if (result === -1) {
|
||||||
|
errorList.push([channelId, channel.url, channel.name])
|
||||||
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')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
finishCount++
|
||||||
this.updateShowProgressBar(false)
|
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]}.`)
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}).finally(_ => {
|
||||||
|
this.updateShowProgressBar(false)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -679,7 +536,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
extensions: ['db']
|
extensions: ['db']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -726,7 +583,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
extensions: ['json']
|
extensions: ['json']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -799,7 +656,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
extensions: ['opml']
|
extensions: ['opml']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -851,7 +708,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
extensions: ['csv']
|
extensions: ['csv']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -860,8 +717,8 @@ export default Vue.extend({
|
||||||
this.profileList[0].subscriptions.forEach((channel) => {
|
this.profileList[0].subscriptions.forEach((channel) => {
|
||||||
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
||||||
let channelName = channel.name
|
let channelName = channel.name
|
||||||
if (channelName.search(',') !== -1) { // add quotations if channel has comma in name
|
if (channelName.search(',') !== -1) { // add quotations and escape existing quotations if channel has comma in name
|
||||||
channelName = `"${channelName}"`
|
channelName = `"${channelName.replaceAll('"', '""')}"`
|
||||||
}
|
}
|
||||||
exportText += `${channel.id},${channelUrl},${channelName}\n`
|
exportText += `${channel.id},${channelUrl},${channelName}\n`
|
||||||
})
|
})
|
||||||
|
@ -896,7 +753,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Subscription File'),
|
||||||
extensions: ['json']
|
extensions: ['json']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -942,23 +799,12 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
checkForLegacySubscriptions: async function () {
|
|
||||||
let dbLocation = await this.getUserDataPath()
|
|
||||||
dbLocation = dbLocation + '/subscriptions.db'
|
|
||||||
this.handleFreetubeImportFile({ canceled: false, filePaths: [dbLocation] })
|
|
||||||
fs.unlink(dbLocation, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
importHistory: async function () {
|
importHistory: async function () {
|
||||||
const options = {
|
const options = {
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.History File'),
|
||||||
extensions: ['db']
|
extensions: ['db']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1040,7 +886,7 @@ export default Vue.extend({
|
||||||
defaultPath: exportFileName,
|
defaultPath: exportFileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Playlist File'),
|
||||||
extensions: ['db']
|
extensions: ['db']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1084,7 +930,7 @@ export default Vue.extend({
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Database File',
|
name: this.$t('Settings.Data Settings.Playlist File'),
|
||||||
extensions: ['db']
|
extensions: ['db']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1329,6 +1175,63 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
},
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'invidiousAPICall',
|
'invidiousAPICall',
|
||||||
'updateProfile',
|
'updateProfile',
|
||||||
|
|
|
@ -5,12 +5,7 @@
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-button
|
<ft-button
|
||||||
:label="$t('Settings.Data Settings.Import Subscriptions')"
|
:label="$t('Settings.Data Settings.Import Subscriptions')"
|
||||||
@click="showImportSubscriptionsPrompt = true"
|
@click="importSubscriptions"
|
||||||
/>
|
|
||||||
<ft-button
|
|
||||||
v-if="usingElectron"
|
|
||||||
:label="$t('Settings.Data Settings.Check for Legacy Subscriptions')"
|
|
||||||
@click="checkForLegacySubscriptions"
|
|
||||||
/>
|
/>
|
||||||
<ft-button
|
<ft-button
|
||||||
:label="$t('Settings.Data Settings.Export Subscriptions')"
|
:label="$t('Settings.Data Settings.Export Subscriptions')"
|
||||||
|
@ -51,13 +46,6 @@
|
||||||
@click="exportPlaylists"
|
@click="exportPlaylists"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<ft-prompt
|
|
||||||
v-if="showImportSubscriptionsPrompt"
|
|
||||||
:label="$t('Settings.Data Settings.Select Import Type')"
|
|
||||||
:option-names="importSubscriptionsPromptNames"
|
|
||||||
:option-values="subscriptionsPromptValues"
|
|
||||||
@click="importSubscriptions"
|
|
||||||
/>
|
|
||||||
<ft-prompt
|
<ft-prompt
|
||||||
v-if="showExportSubscriptionsPrompt"
|
v-if="showExportSubscriptionsPrompt"
|
||||||
:label="$t('Settings.Data Settings.Select Export Type')"
|
:label="$t('Settings.Data Settings.Select Export Type')"
|
||||||
|
|
|
@ -328,9 +328,9 @@ Settings:
|
||||||
Select Import Type: Select Import Type
|
Select Import Type: Select Import Type
|
||||||
Select Export Type: Select Export Type
|
Select Export Type: Select Export Type
|
||||||
Import Subscriptions: Import Subscriptions
|
Import Subscriptions: Import Subscriptions
|
||||||
Import FreeTube: Import FreeTube
|
Subscription File: Subscription File
|
||||||
Import YouTube: Import YouTube
|
History File: History File
|
||||||
Import NewPipe: Import NewPipe
|
Playlist File: Playlist File
|
||||||
Check for Legacy Subscriptions: Check for Legacy Subscriptions
|
Check for Legacy Subscriptions: Check for Legacy Subscriptions
|
||||||
Export Subscriptions: Export Subscriptions
|
Export Subscriptions: Export Subscriptions
|
||||||
Export FreeTube: Export FreeTube
|
Export FreeTube: Export FreeTube
|
||||||
|
|
Loading…
Reference in New Issue