diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index 7963873a..2321deb8 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -54,6 +54,9 @@ export default Vue.extend({ profileList: function () { return this.$store.getters.getProfileList }, + allPlaylists: function () { + 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') @@ -245,7 +248,6 @@ export default Vue.extend({ 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 = [] @@ -317,8 +319,6 @@ export default Vue.extend({ let textDecode = new TextDecoder('utf-8').decode(data) textDecode = JSON.parse(textDecode) - console.log(textDecode) - const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0])) const subscriptions = [] @@ -1075,6 +1075,168 @@ export default Vue.extend({ }) }, + 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 + Object.keys(video).forEach((videoKey) => { + if (!requiredVideoKeys.includes(videoKey)) { + 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') + }) + }) + }, + async convertOldFreeTubeFormatToNew(oldData) { const convertedData = [] for (const channel of oldData) { @@ -1178,7 +1340,9 @@ export default Vue.extend({ 'calculateColorLuminance', 'showOpenDialog', 'showSaveDialog', - 'getUserDataPath' + 'getUserDataPath', + 'addPlaylist', + 'addVideo' ]), ...mapMutations([ diff --git a/src/renderer/components/data-settings/data-settings.vue b/src/renderer/components/data-settings/data-settings.vue index e737e2fe..c4b33595 100644 --- a/src/renderer/components/data-settings/data-settings.vue +++ b/src/renderer/components/data-settings/data-settings.vue @@ -44,6 +44,16 @@ @click="openProfileSettings" /> + + + + -
- - - - -
+ + + + + diff --git a/src/renderer/sass-partials/_settings.sass b/src/renderer/sass-partials/_settings.sass index 1674529a..af92776f 100644 --- a/src/renderer/sass-partials/_settings.sass +++ b/src/renderer/sass-partials/_settings.sass @@ -21,6 +21,9 @@ details width: 85% margin: 0 auto + &[open] + padding-bottom: 15px + hr width: 100% height: 2px @@ -32,7 +35,7 @@ details display: block cursor: pointer padding: 1px 1px 1px 1px - + h3 margin-left: 2% diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 0841c8f6..579f709a 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -282,6 +282,8 @@ Settings: Export NewPipe: Export NewPipe Import History: Import History Export History: Export History + Import Playlists: Import Playlists + Export Playlists: Export Playlists Profile object has insufficient data, skipping item: Profile object has insufficient data, skipping item All subscriptions and profiles have been successfully imported: All subscriptions @@ -301,6 +303,11 @@ Settings: successfully imported All watched history has been successfully exported: All watched history has been successfully exported + Playlist insufficient data: Insufficient data for "$" playlist, skipping item + All playlists has been successfully imported: All playlists has been + successfully imported + All playlists has been successfully exported: All playlists has been + successfully exported Unable to read file: Unable to read file Unable to write file: Unable to write file Unknown data key: Unknown data key