-
-
Notifications
You must be signed in to change notification settings - Fork 679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Option to export/import watch history to db. #2118
Open
ai25
wants to merge
2
commits into
TeamPiped:master
Choose a base branch
from
ai25:history-export
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
<template> | ||
<ModalComponent> | ||
<div class="min-w-max flex flex-col"> | ||
<h2 class="text-xl font-bold mb-4 text-center">Export History</h2> | ||
<form> | ||
<div> | ||
<label class="mr-2" for="export-format">Export as:</label> | ||
<select class="select" id="export-format" v-model="exportAs"> | ||
<option | ||
v-for="option in exportOptions" | ||
:key="option" | ||
:value="option" | ||
v-text="formatField(option)" | ||
/> | ||
</select> | ||
</div> | ||
<div v-if="exportAs === 'history'"> | ||
<label v-for="field in fields" :key="field" class="flex gap-2 items-center"> | ||
<input | ||
class="checkbox" | ||
type="checkbox" | ||
:value="field" | ||
v-model="selectedFields" | ||
:disabled="field === 'videoId'" | ||
/> | ||
<span v-text="formatField(field)" /> | ||
</label> | ||
</div> | ||
</form> | ||
<button class="btn mt-4" @click="handleExport">Export</button> | ||
</div> | ||
</ModalComponent> | ||
</template> | ||
|
||
<script> | ||
import ModalComponent from "./ModalComponent.vue"; | ||
|
||
export default { | ||
components: { | ||
ModalComponent, | ||
}, | ||
data() { | ||
return { | ||
exportOptions: ["playlist", "history"], | ||
exportAs: "playlist", | ||
fields: [ | ||
"videoId", | ||
"title", | ||
"uploaderName", | ||
"uploaderUrl", | ||
"duration", | ||
"thumbnail", | ||
"watchedAt", | ||
"currentTime", | ||
], | ||
selectedFields: [ | ||
"videoId", | ||
"title", | ||
"uploaderName", | ||
"uploaderUrl", | ||
"duration", | ||
"thumbnail", | ||
"watchedAt", | ||
"currentTime", | ||
], | ||
}; | ||
}, | ||
methods: { | ||
async fetchAllVideos() { | ||
if (window.db) { | ||
var tx = window.db.transaction("watch_history", "readonly"); | ||
var store = tx.objectStore("watch_history"); | ||
const request = store.getAll(); | ||
return new Promise((resolve, reject) => { | ||
(request.onsuccess = e => { | ||
const videos = e.target.result; | ||
this.exportVideos = videos; | ||
resolve(); | ||
}), | ||
(request.onerror = e => { | ||
reject(e); | ||
}); | ||
}); | ||
} | ||
}, | ||
handleExport() { | ||
if (this.exportAs === "playlist") { | ||
this.fetchAllVideos() | ||
.then(() => { | ||
this.exportAsPlaylist(); | ||
}) | ||
.catch(e => { | ||
console.error(e); | ||
}); | ||
} else if (this.exportAs === "history") { | ||
this.fetchAllVideos() | ||
.then(() => { | ||
this.exportAsHistory(); | ||
}) | ||
.catch(e => { | ||
console.error(e); | ||
}); | ||
} | ||
}, | ||
exportAsPlaylist() { | ||
const dateStr = new Date().toISOString().split(".")[0]; | ||
let json = { | ||
format: "Piped", | ||
version: 1, | ||
playlists: [ | ||
{ | ||
name: `Piped History ${dateStr}`, | ||
type: "history", | ||
visibility: "private", | ||
videos: this.exportVideos.map(video => "https://youtube.com" + video.url), | ||
}, | ||
], | ||
}; | ||
this.download(JSON.stringify(json), `piped_history_${dateStr}.json`, "application/json"); | ||
}, | ||
exportAsHistory() { | ||
const dateStr = new Date().toISOString().split(".")[0]; | ||
let json = { | ||
format: "Piped", | ||
version: 1, | ||
watchHistory: this.exportVideos.map(video => { | ||
let obj = {}; | ||
this.selectedFields.forEach(field => { | ||
obj[field] = video[field]; | ||
}); | ||
return obj; | ||
}), | ||
}; | ||
this.download(JSON.stringify(json), `piped_history_${dateStr}.json`, "application/json"); | ||
}, | ||
formatField(field) { | ||
// camelCase to Title Case | ||
return field.replace(/([A-Z])/g, " $1").replace(/^./, str => str.toUpperCase()); | ||
}, | ||
}, | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<template> | ||
<ModalComponent> | ||
<div class="text-center"> | ||
<h2 class="text-xl font-bold mb-4 text-center">Import History</h2> | ||
<form> | ||
<br /> | ||
<div> | ||
<input class="btn ml-2 mb-2" ref="fileSelector" type="file" @change="fileChange" /> | ||
</div> | ||
<div> | ||
<strong v-text="`Found ${itemsLength} items`" /> | ||
</div> | ||
<div> | ||
<strong class="flex gap-2 justify-center items-center"> | ||
Override: <input v-model="override" class="checkbox" type="checkbox" /> | ||
</strong> | ||
</div> | ||
<br /> | ||
<div> | ||
<progress :value="index" :max="itemsLength" /> | ||
<div v-text="`Success: ${success} Error: ${error} Skipped: ${skipped}`" /> | ||
</div> | ||
<br /> | ||
<div> | ||
<a class="btn w-auto" @click="handleImport">Import</a> | ||
</div> | ||
</form> | ||
</div> | ||
</ModalComponent> | ||
</template> | ||
<script> | ||
import ModalComponent from "./ModalComponent.vue"; | ||
|
||
export default { | ||
components: { ModalComponent }, | ||
data() { | ||
return { | ||
items: [], | ||
override: false, | ||
index: 0, | ||
success: 0, | ||
error: 0, | ||
skipped: 0, | ||
}; | ||
}, | ||
computed: { | ||
itemsLength() { | ||
return this.items.length; | ||
}, | ||
}, | ||
methods: { | ||
fileChange() { | ||
const file = this.$refs.fileSelector.files[0]; | ||
file.text().then(text => { | ||
this.items = []; | ||
const json = JSON.parse(text); | ||
const items = json.watchHistory.map(video => { | ||
return { | ||
...video, | ||
watchedAt: video.watchedAt ?? 0, | ||
currentTime: video.currentTime ?? 0, | ||
}; | ||
}); | ||
this.items = items.sort((a, b) => b.watchedAt - a.watchedAt); | ||
}); | ||
}, | ||
handleImport() { | ||
if (window.db) { | ||
var tx = window.db.transaction("watch_history", "readwrite"); | ||
var store = tx.objectStore("watch_history"); | ||
this.items.forEach(item => { | ||
const dbItem = store.get(item.videoId); | ||
dbItem.onsuccess = () => { | ||
if (dbItem.result && dbItem.result.videoId === item.videoId) { | ||
if (!this.override) { | ||
this.index++; | ||
this.skipped++; | ||
return; | ||
} | ||
} | ||
try { | ||
const request = store.put(JSON.parse(JSON.stringify(item))); // prevent "Symbol could not be cloned." error | ||
request.onsuccess = () => { | ||
this.index++; | ||
this.success++; | ||
}; | ||
request.onerror = () => { | ||
this.index++; | ||
this.error++; | ||
}; | ||
} catch (error) { | ||
console.error(error); | ||
this.index++; | ||
this.error++; | ||
} | ||
}; | ||
}); | ||
} | ||
}, | ||
}, | ||
}; | ||
</script> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We likely don't want this to be user selectable, as we want to stick to a standard format between frontend implementations.
We would potentially align to a new format in the distant future to allow inter-compatibility with other frontends once a format is standardized - UniversalPipeWrench/unified-user-data-format#1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see one problem with this: What if the format we decide on is not what users want?
For example, if we include watchedAt and currentTime by default this might be a privacy concern for some users. On the other hand, if we decide to leave them out we are throwing out features which I would assume most users want, myself included.
The only solution I can think of is for the standard format to allow these to be optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel
currentTime
is always necessary since otherwise, we can't show any indicators on videos (just the watch history page).Regarding
watchedAt
, that's a fair point, but we don't know how to sort videos then.However, I feel this is less of an issue since watch history is stored entirely locally - so, it's unlikely any third party gets access to it in the first place.