Add ability to import / export playlists and slightly tweak settings layout
This commit is contained in:
parent
907679f440
commit
d21a7f1c24
|
@ -54,6 +54,9 @@ export default Vue.extend({
|
||||||
profileList: function () {
|
profileList: function () {
|
||||||
return this.$store.getters.getProfileList
|
return this.$store.getters.getProfileList
|
||||||
},
|
},
|
||||||
|
allPlaylists: function () {
|
||||||
|
return this.$store.getters.getAllPlaylists
|
||||||
|
},
|
||||||
importSubscriptionsPromptNames: function () {
|
importSubscriptionsPromptNames: function () {
|
||||||
const importFreeTube = this.$t('Settings.Data Settings.Import FreeTube')
|
const importFreeTube = this.$t('Settings.Data Settings.Import FreeTube')
|
||||||
const importYouTube = this.$t('Settings.Data Settings.Import YouTube')
|
const importYouTube = this.$t('Settings.Data Settings.Import YouTube')
|
||||||
|
@ -245,7 +248,6 @@ export default Vue.extend({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const textDecode = new TextDecoder('utf-8').decode(data)
|
const textDecode = new TextDecoder('utf-8').decode(data)
|
||||||
console.log(textDecode)
|
|
||||||
const youtubeSubscriptions = textDecode.split('\n')
|
const youtubeSubscriptions = textDecode.split('\n')
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
const subscriptions = []
|
const subscriptions = []
|
||||||
|
@ -317,8 +319,6 @@ export default Vue.extend({
|
||||||
let textDecode = new TextDecoder('utf-8').decode(data)
|
let textDecode = new TextDecoder('utf-8').decode(data)
|
||||||
textDecode = JSON.parse(textDecode)
|
textDecode = JSON.parse(textDecode)
|
||||||
|
|
||||||
console.log(textDecode)
|
|
||||||
|
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
const subscriptions = []
|
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) {
|
async convertOldFreeTubeFormatToNew(oldData) {
|
||||||
const convertedData = []
|
const convertedData = []
|
||||||
for (const channel of oldData) {
|
for (const channel of oldData) {
|
||||||
|
@ -1178,7 +1340,9 @@ export default Vue.extend({
|
||||||
'calculateColorLuminance',
|
'calculateColorLuminance',
|
||||||
'showOpenDialog',
|
'showOpenDialog',
|
||||||
'showSaveDialog',
|
'showSaveDialog',
|
||||||
'getUserDataPath'
|
'getUserDataPath',
|
||||||
|
'addPlaylist',
|
||||||
|
'addVideo'
|
||||||
]),
|
]),
|
||||||
|
|
||||||
...mapMutations([
|
...mapMutations([
|
||||||
|
|
|
@ -44,6 +44,16 @@
|
||||||
@click="openProfileSettings"
|
@click="openProfileSettings"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-button
|
||||||
|
:label="$t('Settings.Data Settings.Import Playlists')"
|
||||||
|
@click="importPlaylists"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
:label="$t('Settings.Data Settings.Export Playlists')"
|
||||||
|
@click="exportPlaylists"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
<ft-prompt
|
<ft-prompt
|
||||||
v-if="showImportSubscriptionsPrompt"
|
v-if="showImportSubscriptionsPrompt"
|
||||||
:label="$t('Settings.Data Settings.Select Import Type')"
|
:label="$t('Settings.Data Settings.Select Import Type')"
|
||||||
|
|
|
@ -13,23 +13,25 @@
|
||||||
@change="handleDownloadingSettingChange"
|
@change="handleDownloadingSettingChange"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<div
|
<ft-flex-box
|
||||||
v-if="!askForDownloadPath"
|
v-if="!askForDownloadPath"
|
||||||
>
|
>
|
||||||
<ft-flex-box>
|
<ft-input
|
||||||
<ft-input
|
class="folderDisplay"
|
||||||
class="folderDisplay"
|
:placeholder="downloadPath"
|
||||||
:placeholder="downloadPath"
|
:show-action-button="false"
|
||||||
:show-action-button="false"
|
:show-label="false"
|
||||||
:show-label="false"
|
:disabled="true"
|
||||||
:disabled="true"
|
/>
|
||||||
/>
|
</ft-flex-box>
|
||||||
<ft-button
|
<ft-flex-box
|
||||||
:label="$t('Settings.Download Settings.Choose Path')"
|
v-if="!askForDownloadPath"
|
||||||
@click="chooseDownloadingFolder"
|
>
|
||||||
/>
|
<ft-button
|
||||||
</ft-flex-box>
|
:label="$t('Settings.Download Settings.Choose Path')"
|
||||||
</div>
|
@click="chooseDownloadingFolder"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,9 @@ details
|
||||||
width: 85%
|
width: 85%
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
|
|
||||||
|
&[open]
|
||||||
|
padding-bottom: 15px
|
||||||
|
|
||||||
hr
|
hr
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 2px
|
height: 2px
|
||||||
|
@ -32,7 +35,7 @@ details
|
||||||
display: block
|
display: block
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
padding: 1px 1px 1px 1px
|
padding: 1px 1px 1px 1px
|
||||||
|
|
||||||
h3
|
h3
|
||||||
margin-left: 2%
|
margin-left: 2%
|
||||||
|
|
||||||
|
|
|
@ -282,6 +282,8 @@ Settings:
|
||||||
Export NewPipe: Export NewPipe
|
Export NewPipe: Export NewPipe
|
||||||
Import History: Import History
|
Import History: Import History
|
||||||
Export History: Export History
|
Export History: Export History
|
||||||
|
Import Playlists: Import Playlists
|
||||||
|
Export Playlists: Export Playlists
|
||||||
Profile object has insufficient data, skipping item: Profile object has insufficient
|
Profile object has insufficient data, skipping item: Profile object has insufficient
|
||||||
data, skipping item
|
data, skipping item
|
||||||
All subscriptions and profiles have been successfully imported: All subscriptions
|
All subscriptions and profiles have been successfully imported: All subscriptions
|
||||||
|
@ -301,6 +303,11 @@ Settings:
|
||||||
successfully imported
|
successfully imported
|
||||||
All watched history has been successfully exported: All watched history has been
|
All watched history has been successfully exported: All watched history has been
|
||||||
successfully exported
|
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 read file: Unable to read file
|
||||||
Unable to write file: Unable to write file
|
Unable to write file: Unable to write file
|
||||||
Unknown data key: Unknown data key
|
Unknown data key: Unknown data key
|
||||||
|
|
Loading…
Reference in New Issue