Import from YouTube history using JSON export (#2958)

* Initial implementation of import from YouTube history
(Using JSON  export)

* Properly escaping hostnames in regular expressions

* support other locales

* Apply suggestions from code review

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* support json import through sane button, bug fix

* remove `import youtube history` translations

* dont save length or view count for imported youtube history

---------

Co-authored-by: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
This commit is contained in:
Alexandre Rocha Lima e Marcondes 2023-03-30 17:51:37 +02:00 committed by GitHub
parent d4bc2b8727
commit 7a991cd05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 2 deletions

View File

@ -679,7 +679,7 @@ export default defineComponent({
filters: [
{
name: this.$t('Settings.Data Settings.History File'),
extensions: ['db']
extensions: ['db', 'json']
}
]
}
@ -696,7 +696,17 @@ export default defineComponent({
showToast(`${message}: ${err}`)
return
}
textDecode = textDecode.split('\n')
response.filePaths.forEach(filePath => {
if (filePath.endsWith('.db')) {
this.importFreeTubeSubscriptions(textDecode.split('\n'))
} else if (filePath.endsWith('.json')) {
this.importYouTubeHistory(JSON.parse(textDecode))
}
})
},
importFreeTubeHistory(textDecode) {
textDecode.pop()
textDecode.forEach((history) => {
@ -741,6 +751,90 @@ export default defineComponent({
showToast(this.$t('Settings.Data Settings.All watched history has been successfully imported'))
},
importYouTubeHistory(historyData) {
const filterPredicate = item =>
item.products.includes('YouTube') &&
item.titleUrl != null && // removed video doesnt contain url...
item.titleUrl.includes('www.youtube.com/watch?v') &&
item.details == null // dont import ads
const filteredHistoryData = historyData.filter(filterPredicate)
// remove 'Watched' and translated variants from start of title
// so we get the common string prefix for all the titles
const getCommonStart = (allTitles) => {
const watchedTitle = allTitles[0].split(' ')
allTitles.forEach((title) => {
const splitTitle = title.split(' ')
for (let wtIndex = 0; wtIndex <= watchedTitle.length; wtIndex++) {
if (!splitTitle.includes(watchedTitle[wtIndex])) {
watchedTitle.splice(wtIndex, watchedTitle.length - wtIndex)
}
}
})
return watchedTitle.join(' ')
}
const commonStart = getCommonStart(filteredHistoryData.map(e => e.title))
// 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 keyMapping = {
title: [{ importKey: 'title', predicate: item => item.slice(commonStart.length) }], // Removes the "Watched " term on the title
titleUrl: [{ importKey: 'videoId', predicate: item => item.replaceAll(/https:\/\/www\.youtube\.com\/watch\?v=/gi, '') }], // Extracts the video ID
time: [{ importKey: 'timeWatched', predicate: item => new Date(item).valueOf() }],
subtitles: [
{ importKey: 'author', predicate: item => item[0].name ?? '' },
{ importKey: 'authorId', predicate: item => item[0].url?.replaceAll(/https:\/\/www\.youtube\.com\/channel\//gi, '') ?? '' },
],
}
const knownKeys = [
'header',
'description',
'products',
'details',
'activityControls',
].concat(Object.keys(keyMapping))
filteredHistoryData.forEach(element => {
const historyObject = {}
Object.keys(element).forEach((key) => {
if (!knownKeys.includes(key)) {
showToast(`Unknown data key: ${key}`)
} else {
const mapping = keyMapping[key]
if (mapping && Array.isArray(mapping)) {
mapping.forEach(item => {
historyObject[item.importKey] = item.predicate(element[key])
})
}
}
})
if (Object.keys(historyObject).length < keyMapping.length - 1) {
showToast(this.$t('Settings.Data Settings.History object has insufficient data, skipping item'))
} else {
// YouTube history export does not have this data, setting some defaults.
historyObject.type = 'video'
historyObject.published = historyObject.timeWatched ?? 1
historyObject.description = ''
historyObject.lengthSeconds = null
historyObject.watchProgress = 1
historyObject.isLive = false
historyObject.paid = false
this.updateHistory(historyObject)
}
})
showToast(this.$t('Settings.Data Settings.All watched history has been successfully imported'))
},
exportHistory: async function () {
const historyDb = this.historyCache.map((historyEntry) => {
return JSON.stringify(historyEntry)

View File

@ -11,6 +11,8 @@
:label="$t('Settings.Data Settings.Export Subscriptions')"
@click="showExportSubscriptionsPrompt = true"
/>
</ft-flex-box>
<ft-flex-box>
<ft-button
:label="$t('Settings.Data Settings.Import History')"
@click="importHistory"