[Feature] Add support for importing/exporting csv YouTube subscriptions + improve speed of reimporting subscriptions (#1543)
* Add support for csv yt subscriptions * Simplify setting exportFileName * check if subscribed to channel before making web requests Co-authored-by: Preston <freetubeapp@protonmail.com>
This commit is contained in:
parent
3b676cbeef
commit
044e5bf907
|
@ -27,6 +27,7 @@ export default Vue.extend({
|
|||
showExportSubscriptionsPrompt: false,
|
||||
subscriptionsPromptValues: [
|
||||
'freetube',
|
||||
'youtubenew',
|
||||
'youtube',
|
||||
'youtubeold',
|
||||
'newpipe'
|
||||
|
@ -58,6 +59,7 @@ export default Vue.extend({
|
|||
const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe')
|
||||
return [
|
||||
`${importFreeTube} (.db)`,
|
||||
`${importYouTube} (.csv)`,
|
||||
`${importYouTube} (.json)`,
|
||||
`${importYouTube} (.opml)`,
|
||||
`${importNewPipe} (.json)`
|
||||
|
@ -69,6 +71,7 @@ export default Vue.extend({
|
|||
const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe')
|
||||
return [
|
||||
`${exportFreeTube} (.db)`,
|
||||
`${exportYouTube} (.csv)`,
|
||||
`${exportYouTube} (.json)`,
|
||||
`${exportYouTube} (.opml)`,
|
||||
`${exportNewPipe} (.json)`
|
||||
|
@ -93,6 +96,9 @@ export default Vue.extend({
|
|||
case 'freetube':
|
||||
this.importFreeTubeSubscriptions()
|
||||
break
|
||||
case 'youtubenew':
|
||||
this.importCsvYouTubeSubscriptions()
|
||||
break
|
||||
case 'youtube':
|
||||
this.importYouTubeSubscriptions()
|
||||
break
|
||||
|
@ -228,6 +234,75 @@ export default Vue.extend({
|
|||
this.handleFreetubeImportFile(filePath)
|
||||
},
|
||||
|
||||
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)
|
||||
console.log(textDecode)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleYoutubeImportFile: function (filePath) {
|
||||
fs.readFile(filePath, async (err, data) => {
|
||||
if (err) {
|
||||
|
@ -310,6 +385,25 @@ export default Vue.extend({
|
|||
})
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
importYouTubeSubscriptions: async function () {
|
||||
const options = {
|
||||
properties: ['openFile'],
|
||||
|
@ -387,6 +481,10 @@ export default Vue.extend({
|
|||
|
||||
feedData.forEach(async (channel, index) => {
|
||||
const channelId = channel.xmlurl.replace('https://www.youtube.com/feeds/videos.xml?channel_id=', '')
|
||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
||||
return sub.id === channelId
|
||||
})
|
||||
if (subExists === -1) {
|
||||
let channelInfo
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channelInfo = await this.getChannelInfoInvidious(channelId)
|
||||
|
@ -400,12 +498,6 @@ export default Vue.extend({
|
|||
name: channelInfo.author,
|
||||
thumbnail: channelInfo.authorThumbnails[1].url
|
||||
}
|
||||
|
||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
||||
return sub.id === subscription.id || sub.name === subscription.name
|
||||
})
|
||||
|
||||
if (subExists === -1) {
|
||||
subscriptions.push(subscription)
|
||||
}
|
||||
}
|
||||
|
@ -498,6 +590,11 @@ export default Vue.extend({
|
|||
|
||||
newPipeSubscriptions.forEach(async (channel, index) => {
|
||||
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
|
||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
||||
return sub.id === channelId
|
||||
})
|
||||
|
||||
if (subExists === -1) {
|
||||
let channelInfo
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channelInfo = await this.getChannelInfoInvidious(channelId)
|
||||
|
@ -511,12 +608,6 @@ export default Vue.extend({
|
|||
name: channelInfo.author,
|
||||
thumbnail: channelInfo.authorThumbnails[1].url
|
||||
}
|
||||
|
||||
const subExists = primaryProfile.subscriptions.findIndex((sub) => {
|
||||
return sub.id === subscription.id || sub.name === subscription.name
|
||||
})
|
||||
|
||||
if (subExists === -1) {
|
||||
subscriptions.push(subscription)
|
||||
}
|
||||
}
|
||||
|
@ -557,6 +648,9 @@ export default Vue.extend({
|
|||
case 'freetube':
|
||||
this.exportFreeTubeSubscriptions()
|
||||
break
|
||||
case 'youtubenew':
|
||||
this.exportCsvYouTubeSubscriptions()
|
||||
break
|
||||
case 'youtube':
|
||||
this.exportYouTubeSubscriptions()
|
||||
break
|
||||
|
@ -573,21 +667,8 @@ export default Vue.extend({
|
|||
await this.compactProfiles()
|
||||
const userData = await this.getUserDataPath()
|
||||
const subscriptionsDb = `${userData}/profiles.db`
|
||||
const date = new Date()
|
||||
let dateMonth = date.getMonth() + 1
|
||||
|
||||
if (dateMonth < 10) {
|
||||
dateMonth = '0' + dateMonth
|
||||
}
|
||||
|
||||
let dateDay = date.getDate()
|
||||
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay
|
||||
}
|
||||
|
||||
const dateYear = date.getFullYear()
|
||||
const exportFileName = 'freetube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'freetube-subscriptions-' + date + '.db'
|
||||
|
||||
const options = {
|
||||
defaultPath: exportFileName,
|
||||
|
@ -633,21 +714,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
exportYouTubeSubscriptions: async function () {
|
||||
const date = new Date()
|
||||
let dateMonth = date.getMonth() + 1
|
||||
|
||||
if (dateMonth < 10) {
|
||||
dateMonth = '0' + dateMonth
|
||||
}
|
||||
|
||||
let dateDay = date.getDate()
|
||||
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay
|
||||
}
|
||||
|
||||
const dateYear = date.getFullYear()
|
||||
const exportFileName = 'youtube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.json'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'youtube-subscriptions-' + date + '.json'
|
||||
|
||||
const options = {
|
||||
defaultPath: exportFileName,
|
||||
|
@ -719,21 +787,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
exportOpmlYouTubeSubscriptions: async function () {
|
||||
const date = new Date()
|
||||
let dateMonth = date.getMonth() + 1
|
||||
|
||||
if (dateMonth < 10) {
|
||||
dateMonth = '0' + dateMonth
|
||||
}
|
||||
|
||||
let dateDay = date.getDate()
|
||||
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay
|
||||
}
|
||||
|
||||
const dateYear = date.getFullYear()
|
||||
const exportFileName = 'youtube-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.opml'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'youtube-subscriptions-' + date + '.opml'
|
||||
|
||||
const options = {
|
||||
defaultPath: exportFileName,
|
||||
|
@ -783,22 +838,50 @@ export default Vue.extend({
|
|||
})
|
||||
},
|
||||
|
||||
exportCsvYouTubeSubscriptions: async function () {
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'youtube-subscriptions-' + date + '.csv'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
this.showToast({
|
||||
message: this.$t('Settings.Data Settings.Subscriptions have been successfully exported')
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
exportNewPipeSubscriptions: async function () {
|
||||
const date = new Date()
|
||||
let dateMonth = date.getMonth() + 1
|
||||
|
||||
if (dateMonth < 10) {
|
||||
dateMonth = '0' + dateMonth
|
||||
}
|
||||
|
||||
let dateDay = date.getDate()
|
||||
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay
|
||||
}
|
||||
|
||||
const dateYear = date.getFullYear()
|
||||
const exportFileName = 'newpipe-subscriptions-' + dateYear + '-' + dateMonth + '-' + dateDay + '.json'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'newpipe-subscriptions-' + date + '.json'
|
||||
|
||||
const options = {
|
||||
defaultPath: exportFileName,
|
||||
|
@ -945,21 +1028,8 @@ export default Vue.extend({
|
|||
await this.compactHistory()
|
||||
const userData = await this.getUserDataPath()
|
||||
const historyDb = `${userData}/history.db`
|
||||
const date = new Date()
|
||||
let dateMonth = date.getMonth() + 1
|
||||
|
||||
if (dateMonth < 10) {
|
||||
dateMonth = '0' + dateMonth
|
||||
}
|
||||
|
||||
let dateDay = date.getDate()
|
||||
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay
|
||||
}
|
||||
|
||||
const dateYear = date.getFullYear()
|
||||
const exportFileName = 'freetube-history-' + dateYear + '-' + dateMonth + '-' + dateDay + '.db'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportFileName = 'freetube-history-' + date + '.db'
|
||||
|
||||
const options = {
|
||||
defaultPath: exportFileName,
|
||||
|
|
Loading…
Reference in New Issue