[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:
ChunkyProgrammer 2021-08-25 03:47:21 -04:00 committed by GitHub
parent 3b676cbeef
commit 044e5bf907
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 178 additions and 108 deletions

View File

@ -27,6 +27,7 @@ export default Vue.extend({
showExportSubscriptionsPrompt: false, showExportSubscriptionsPrompt: false,
subscriptionsPromptValues: [ subscriptionsPromptValues: [
'freetube', 'freetube',
'youtubenew',
'youtube', 'youtube',
'youtubeold', 'youtubeold',
'newpipe' 'newpipe'
@ -58,6 +59,7 @@ export default Vue.extend({
const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe') const importNewPipe = this.$t('Settings.Data Settings.Import NewPipe')
return [ return [
`${importFreeTube} (.db)`, `${importFreeTube} (.db)`,
`${importYouTube} (.csv)`,
`${importYouTube} (.json)`, `${importYouTube} (.json)`,
`${importYouTube} (.opml)`, `${importYouTube} (.opml)`,
`${importNewPipe} (.json)` `${importNewPipe} (.json)`
@ -69,6 +71,7 @@ export default Vue.extend({
const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe') const exportNewPipe = this.$t('Settings.Data Settings.Export NewPipe')
return [ return [
`${exportFreeTube} (.db)`, `${exportFreeTube} (.db)`,
`${exportYouTube} (.csv)`,
`${exportYouTube} (.json)`, `${exportYouTube} (.json)`,
`${exportYouTube} (.opml)`, `${exportYouTube} (.opml)`,
`${exportNewPipe} (.json)` `${exportNewPipe} (.json)`
@ -93,6 +96,9 @@ export default Vue.extend({
case 'freetube': case 'freetube':
this.importFreeTubeSubscriptions() this.importFreeTubeSubscriptions()
break break
case 'youtubenew':
this.importCsvYouTubeSubscriptions()
break
case 'youtube': case 'youtube':
this.importYouTubeSubscriptions() this.importYouTubeSubscriptions()
break break
@ -228,6 +234,75 @@ export default Vue.extend({
this.handleFreetubeImportFile(filePath) 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) { handleYoutubeImportFile: function (filePath) {
fs.readFile(filePath, async (err, data) => { fs.readFile(filePath, async (err, data) => {
if (err) { 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 () { importYouTubeSubscriptions: async function () {
const options = { const options = {
properties: ['openFile'], properties: ['openFile'],
@ -387,25 +481,23 @@ 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=', '')
let channelInfo const subExists = primaryProfile.subscriptions.findIndex((sub) => {
if (this.backendPreference === 'invidious') { return sub.id === channelId
channelInfo = await this.getChannelInfoInvidious(channelId) })
} else { if (subExists === -1) {
channelInfo = await this.getChannelInfoLocal(channelId) let channelInfo
} if (this.backendPreference === 'invidious') {
channelInfo = await this.getChannelInfoInvidious(channelId)
if (typeof channelInfo.author !== 'undefined') { } else {
const subscription = { channelInfo = await this.getChannelInfoLocal(channelId)
id: channelId,
name: channelInfo.author,
thumbnail: channelInfo.authorThumbnails[1].url
} }
const subExists = primaryProfile.subscriptions.findIndex((sub) => { if (typeof channelInfo.author !== 'undefined') {
return sub.id === subscription.id || sub.name === subscription.name const subscription = {
}) id: channelId,
name: channelInfo.author,
if (subExists === -1) { thumbnail: channelInfo.authorThumbnails[1].url
}
subscriptions.push(subscription) subscriptions.push(subscription)
} }
} }
@ -498,25 +590,24 @@ export default Vue.extend({
newPipeSubscriptions.forEach(async (channel, index) => { newPipeSubscriptions.forEach(async (channel, index) => {
const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '') const channelId = channel.url.replace(/https:\/\/(www\.)?youtube\.com\/channel\//, '')
let channelInfo const subExists = primaryProfile.subscriptions.findIndex((sub) => {
if (this.backendPreference === 'invidious') { return sub.id === channelId
channelInfo = await this.getChannelInfoInvidious(channelId) })
} else {
channelInfo = await this.getChannelInfoLocal(channelId)
}
if (typeof channelInfo.author !== 'undefined') { if (subExists === -1) {
const subscription = { let channelInfo
id: channelId, if (this.backendPreference === 'invidious') {
name: channelInfo.author, channelInfo = await this.getChannelInfoInvidious(channelId)
thumbnail: channelInfo.authorThumbnails[1].url } else {
channelInfo = await this.getChannelInfoLocal(channelId)
} }
const subExists = primaryProfile.subscriptions.findIndex((sub) => { if (typeof channelInfo.author !== 'undefined') {
return sub.id === subscription.id || sub.name === subscription.name const subscription = {
}) id: channelId,
name: channelInfo.author,
if (subExists === -1) { thumbnail: channelInfo.authorThumbnails[1].url
}
subscriptions.push(subscription) subscriptions.push(subscription)
} }
} }
@ -557,6 +648,9 @@ export default Vue.extend({
case 'freetube': case 'freetube':
this.exportFreeTubeSubscriptions() this.exportFreeTubeSubscriptions()
break break
case 'youtubenew':
this.exportCsvYouTubeSubscriptions()
break
case 'youtube': case 'youtube':
this.exportYouTubeSubscriptions() this.exportYouTubeSubscriptions()
break break
@ -573,21 +667,8 @@ export default Vue.extend({
await this.compactProfiles() await this.compactProfiles()
const userData = await this.getUserDataPath() const userData = await this.getUserDataPath()
const subscriptionsDb = `${userData}/profiles.db` const subscriptionsDb = `${userData}/profiles.db`
const date = new Date() const date = new Date().toISOString().split('T')[0]
let dateMonth = date.getMonth() + 1 const exportFileName = 'freetube-subscriptions-' + date + '.db'
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 options = { const options = {
defaultPath: exportFileName, defaultPath: exportFileName,
@ -633,21 +714,8 @@ export default Vue.extend({
}, },
exportYouTubeSubscriptions: async function () { exportYouTubeSubscriptions: async function () {
const date = new Date() const date = new Date().toISOString().split('T')[0]
let dateMonth = date.getMonth() + 1 const exportFileName = 'youtube-subscriptions-' + date + '.json'
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 options = { const options = {
defaultPath: exportFileName, defaultPath: exportFileName,
@ -719,21 +787,8 @@ export default Vue.extend({
}, },
exportOpmlYouTubeSubscriptions: async function () { exportOpmlYouTubeSubscriptions: async function () {
const date = new Date() const date = new Date().toISOString().split('T')[0]
let dateMonth = date.getMonth() + 1 const exportFileName = 'youtube-subscriptions-' + date + '.opml'
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 options = { const options = {
defaultPath: exportFileName, 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 () { exportNewPipeSubscriptions: async function () {
const date = new Date() const date = new Date().toISOString().split('T')[0]
let dateMonth = date.getMonth() + 1 const exportFileName = 'newpipe-subscriptions-' + date + '.json'
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 options = { const options = {
defaultPath: exportFileName, defaultPath: exportFileName,
@ -945,21 +1028,8 @@ export default Vue.extend({
await this.compactHistory() await this.compactHistory()
const userData = await this.getUserDataPath() const userData = await this.getUserDataPath()
const historyDb = `${userData}/history.db` const historyDb = `${userData}/history.db`
const date = new Date() const date = new Date().toISOString().split('T')[0]
let dateMonth = date.getMonth() + 1 const exportFileName = 'freetube-history-' + date + '.db'
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 options = { const options = {
defaultPath: exportFileName, defaultPath: exportFileName,