2022-04-07 02:33:25 +00:00
|
|
|
<template>
|
2023-01-06 19:38:42 +00:00
|
|
|
<h2 v-if="authenticated" class="font-bold my-4" v-t="'titles.playlists'" />
|
2022-04-07 02:33:25 +00:00
|
|
|
|
2023-01-06 19:38:42 +00:00
|
|
|
<div v-if="authenticated" class="flex justify-between mb-3">
|
2022-11-28 13:51:17 +00:00
|
|
|
<button v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
|
2022-11-28 13:13:36 +00:00
|
|
|
<div class="flex">
|
2022-11-28 13:51:17 +00:00
|
|
|
<button
|
|
|
|
v-if="this.playlists.length > 0"
|
|
|
|
v-t="'actions.export_to_json'"
|
|
|
|
class="btn"
|
|
|
|
@click="exportPlaylists"
|
|
|
|
/>
|
2023-05-17 07:55:49 +00:00
|
|
|
<input
|
|
|
|
id="fileSelector"
|
|
|
|
ref="fileSelector"
|
|
|
|
type="file"
|
|
|
|
class="display-none"
|
|
|
|
@change="importPlaylists"
|
|
|
|
multiple="multiple"
|
|
|
|
/>
|
2022-11-28 13:51:17 +00:00
|
|
|
<label for="fileSelector" v-t="'actions.import_from_json'" class="btn ml-2" />
|
2022-11-28 13:13:36 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-04-07 02:33:25 +00:00
|
|
|
|
|
|
|
<div class="video-grid">
|
|
|
|
<div v-for="playlist in playlists" :key="playlist.id">
|
|
|
|
<router-link :to="`/playlist?list=${playlist.id}`">
|
|
|
|
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
2022-10-11 05:00:51 +00:00
|
|
|
<div class="relative text-sm">
|
|
|
|
<span
|
|
|
|
class="thumbnail-overlay thumbnail-right"
|
|
|
|
v-text="`${playlist.videos} ${$t('video.videos')}`"
|
|
|
|
/>
|
|
|
|
</div>
|
2022-04-07 02:33:25 +00:00
|
|
|
<p
|
|
|
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
|
|
|
class="my-2 overflow-hidden flex link"
|
|
|
|
:title="playlist.name"
|
|
|
|
v-text="playlist.name"
|
|
|
|
/>
|
|
|
|
</router-link>
|
2023-05-30 12:24:59 +00:00
|
|
|
<button class="btn h-auto" @click="showPlaylistEditModal(playlist)" v-t="'actions.edit_playlist'" />
|
2023-05-30 13:02:20 +00:00
|
|
|
<button class="btn h-auto ml-2" @click="playlistToDelete = playlist.id" v-t="'actions.delete_playlist'" />
|
2023-05-30 12:24:59 +00:00
|
|
|
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
|
|
|
|
<div class="flex flex-col gap-2">
|
|
|
|
<h2 v-t="'actions.edit_playlist'" />
|
|
|
|
<input
|
|
|
|
class="input"
|
|
|
|
type="text"
|
|
|
|
v-model="newPlaylistName"
|
|
|
|
:placeholder="$t('actions.playlist_name')"
|
|
|
|
/>
|
|
|
|
<input
|
|
|
|
class="input"
|
|
|
|
type="text"
|
|
|
|
v-model="newPlaylistDescription"
|
|
|
|
:placeholder="$t('actions.playlist_description')"
|
|
|
|
/>
|
|
|
|
<button class="btn ml-auto" @click="editPlaylist(playlist)" v-t="'actions.okay'" />
|
|
|
|
</div>
|
|
|
|
</ModalComponent>
|
2023-05-30 13:02:20 +00:00
|
|
|
<ConfirmModal
|
|
|
|
v-if="playlistToDelete == playlist.id"
|
|
|
|
:message="$t('actions.delete_playlist_confirm')"
|
|
|
|
@close="playlistToDelete = null"
|
|
|
|
@confirm="deletePlaylist(playlist.id)"
|
|
|
|
/>
|
2022-04-07 02:33:25 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-01-06 19:38:42 +00:00
|
|
|
<hr />
|
|
|
|
|
|
|
|
<h2 class="font-bold my-4" v-t="'titles.bookmarks'" />
|
2023-01-06 18:30:28 +00:00
|
|
|
|
|
|
|
<div v-if="bookmarks" class="video-grid">
|
2023-01-06 19:38:42 +00:00
|
|
|
<router-link
|
|
|
|
v-for="(playlist, index) in bookmarks"
|
|
|
|
:key="playlist.playlistId"
|
|
|
|
:to="`/playlist?list=${playlist.playlistId}`"
|
|
|
|
>
|
2023-01-06 18:30:28 +00:00
|
|
|
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
|
|
|
|
<div class="relative text-sm">
|
|
|
|
<span class="thumbnail-overlay thumbnail-right" v-text="`${playlist.videos} ${$t('video.videos')}`" />
|
2023-01-06 19:38:42 +00:00
|
|
|
<div class="absolute bottom-100px right-5px px-5px z-100" @click.prevent="removeBookmark(index)">
|
|
|
|
<font-awesome-icon class="ml-3" icon="bookmark" />
|
|
|
|
</div>
|
2023-01-06 18:30:28 +00:00
|
|
|
</div>
|
|
|
|
<p
|
|
|
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
|
|
|
class="my-2 overflow-hidden flex link"
|
|
|
|
:title="playlist.name"
|
|
|
|
v-text="playlist.name"
|
|
|
|
/>
|
2023-01-06 20:05:05 +00:00
|
|
|
<a :href="playlist.uploaderUrl" class="flex items-center">
|
2023-01-06 19:38:42 +00:00
|
|
|
<img class="rounded-full w-32px h-32px" :src="playlist.uploaderAvatar" />
|
2023-01-06 20:05:05 +00:00
|
|
|
<span class="ml-3 hover:underline" v-text="playlist.uploader" />
|
|
|
|
</a>
|
2023-01-06 18:30:28 +00:00
|
|
|
</router-link>
|
|
|
|
</div>
|
|
|
|
<br />
|
2022-04-07 02:33:25 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
2023-05-30 13:02:20 +00:00
|
|
|
import ConfirmModal from "./ConfirmModal.vue";
|
2023-05-30 12:24:59 +00:00
|
|
|
import ModalComponent from "./ModalComponent.vue";
|
2023-05-30 13:02:20 +00:00
|
|
|
|
2022-04-07 02:33:25 +00:00
|
|
|
export default {
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
playlists: [],
|
2023-01-06 18:30:28 +00:00
|
|
|
bookmarks: [],
|
2023-05-30 13:02:20 +00:00
|
|
|
playlistToDelete: null,
|
2023-05-30 12:24:59 +00:00
|
|
|
playlistToEdit: null,
|
|
|
|
newPlaylistName: "",
|
|
|
|
newPlaylistDescription: "",
|
2022-04-07 02:33:25 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
if (this.authenticated) this.fetchPlaylists();
|
2023-01-06 18:30:28 +00:00
|
|
|
this.loadPlaylistBookmarks();
|
2022-04-07 02:33:25 +00:00
|
|
|
},
|
|
|
|
activated() {
|
|
|
|
document.title = this.$t("titles.playlists") + " - Piped";
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
fetchPlaylists() {
|
2022-07-21 04:04:57 +00:00
|
|
|
this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
|
2022-04-07 02:33:25 +00:00
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
},
|
|
|
|
}).then(json => {
|
|
|
|
this.playlists = json;
|
|
|
|
});
|
|
|
|
},
|
2023-05-30 12:24:59 +00:00
|
|
|
showPlaylistEditModal(playlist) {
|
|
|
|
this.newPlaylistName = playlist.name;
|
|
|
|
this.newPlaylistDescription = playlist.description;
|
|
|
|
this.playlistToEdit = playlist.id;
|
|
|
|
},
|
|
|
|
editPlaylist(selectedPlaylist) {
|
|
|
|
// save the new name and description since they could be overwritten during the http request
|
|
|
|
const newName = this.newPlaylistName;
|
|
|
|
const newDescription = this.newPlaylistDescription;
|
|
|
|
if (newName != selectedPlaylist.name) {
|
|
|
|
this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify({
|
|
|
|
playlistId: selectedPlaylist.id,
|
|
|
|
newName: newName,
|
|
|
|
}),
|
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
}).then(json => {
|
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else selectedPlaylist.name = newName;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (newDescription != selectedPlaylist.description) {
|
|
|
|
this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, {
|
|
|
|
method: "PATCH",
|
|
|
|
body: JSON.stringify({
|
|
|
|
playlistId: selectedPlaylist.id,
|
|
|
|
description: newDescription,
|
|
|
|
}),
|
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
}).then(json => {
|
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else selectedPlaylist.description = newDescription;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.playlistToEdit = null;
|
2022-08-28 17:58:33 +00:00
|
|
|
},
|
2022-04-07 02:33:25 +00:00
|
|
|
deletePlaylist(id) {
|
2023-05-30 13:02:20 +00:00
|
|
|
this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify({
|
|
|
|
playlistId: id,
|
|
|
|
}),
|
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
}).then(json => {
|
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else this.playlists = this.playlists.filter(playlist => playlist.id !== id);
|
|
|
|
});
|
|
|
|
this.playlistToDelete = null;
|
2022-04-07 02:33:25 +00:00
|
|
|
},
|
2022-11-28 13:51:17 +00:00
|
|
|
onCreatePlaylist() {
|
2022-04-07 02:33:25 +00:00
|
|
|
const name = prompt(this.$t("actions.create_playlist"));
|
2022-11-28 13:51:17 +00:00
|
|
|
if (!name) return;
|
|
|
|
this.createPlaylist(name).then(json => {
|
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else this.fetchPlaylists();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
async createPlaylist(name) {
|
|
|
|
let json = await this.fetchJson(this.authApiUrl() + "/user/playlists/create", null, {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify({
|
|
|
|
name: name,
|
|
|
|
}),
|
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return json;
|
2022-04-07 02:33:25 +00:00
|
|
|
},
|
2022-11-29 18:57:58 +00:00
|
|
|
async exportPlaylists() {
|
2022-11-28 13:13:36 +00:00
|
|
|
if (!this.playlists) return;
|
|
|
|
let json = {
|
2022-11-28 13:53:13 +00:00
|
|
|
format: "Piped",
|
|
|
|
version: 1,
|
2022-11-28 13:13:36 +00:00
|
|
|
playlists: [],
|
|
|
|
};
|
2023-01-01 19:00:22 +00:00
|
|
|
let tasks = this.playlists.map(playlist => this.fetchPlaylistJson(playlist.id));
|
2022-11-29 18:57:58 +00:00
|
|
|
json.playlists = await Promise.all(tasks);
|
|
|
|
this.download(JSON.stringify(json), "playlists.json", "application/json");
|
2022-11-28 13:13:36 +00:00
|
|
|
},
|
2022-11-29 18:57:58 +00:00
|
|
|
async fetchPlaylistJson(playlistId) {
|
2022-11-28 13:13:36 +00:00
|
|
|
let playlist = await this.fetchJson(this.authApiUrl() + "/playlists/" + playlistId);
|
|
|
|
let playlistJson = {
|
|
|
|
name: playlist.name,
|
|
|
|
// possible other types: history, watch later, ...
|
|
|
|
type: "playlist",
|
|
|
|
// as Invidious supports public and private playlists
|
|
|
|
visibility: "private",
|
|
|
|
// list of the videos, starting with "https://youtube.com" to clarify that those are YT videos
|
2023-01-01 19:00:22 +00:00
|
|
|
videos: playlist.relatedStreams.map(stream => "https://youtube.com" + stream.url),
|
2022-11-28 13:13:36 +00:00
|
|
|
};
|
2022-11-29 18:57:58 +00:00
|
|
|
return playlistJson;
|
2022-11-28 13:13:36 +00:00
|
|
|
},
|
2022-11-29 18:57:58 +00:00
|
|
|
async importPlaylists() {
|
2023-05-17 07:55:49 +00:00
|
|
|
const files = this.$refs.fileSelector.files;
|
|
|
|
for (let file of files) {
|
|
|
|
await this.importPlaylistFile(file);
|
|
|
|
}
|
|
|
|
window.location.reload();
|
|
|
|
},
|
|
|
|
async importPlaylistFile(file) {
|
2022-11-29 18:57:58 +00:00
|
|
|
let text = await file.text();
|
2023-01-01 18:57:00 +00:00
|
|
|
let tasks = [];
|
|
|
|
// list of playlists exported from Piped
|
|
|
|
if (text.includes("playlists")) {
|
|
|
|
let playlists = JSON.parse(text).playlists;
|
|
|
|
if (!playlists.length) {
|
|
|
|
alert(this.$t("actions.no_valid_playlists"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (var i = 0; i < playlists.length; i++) {
|
|
|
|
tasks.push(this.createPlaylistWithVideos(playlists[i]));
|
|
|
|
}
|
|
|
|
// CSV from Google Takeout
|
|
|
|
} else if (file.name.slice(-4).toLowerCase() == ".csv") {
|
|
|
|
const lines = text.split("\n");
|
|
|
|
const playlist = {
|
|
|
|
name: lines[1].split(",")[4],
|
|
|
|
videos: lines
|
|
|
|
.slice(4, lines.length)
|
|
|
|
.filter(line => line != "")
|
|
|
|
.map(line => `https://youtube.com/watch?v=${line.split(",")[0]}`),
|
|
|
|
};
|
|
|
|
tasks.push(this.createPlaylistWithVideos(playlist));
|
|
|
|
} else {
|
2022-11-29 18:57:58 +00:00
|
|
|
alert(this.$t("actions.no_valid_playlists"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await Promise.all(tasks);
|
2022-11-28 13:51:17 +00:00
|
|
|
},
|
2022-11-29 18:57:58 +00:00
|
|
|
async createPlaylistWithVideos(playlist) {
|
2022-11-28 13:51:17 +00:00
|
|
|
let newPlaylist = await this.createPlaylist(playlist.name);
|
2022-11-29 18:57:58 +00:00
|
|
|
let videoIds = playlist.videos.map(url => url.substr(-11));
|
2022-11-28 13:51:17 +00:00
|
|
|
await this.addVideosToPlaylist(newPlaylist.playlistId, videoIds);
|
|
|
|
},
|
|
|
|
async addVideosToPlaylist(playlistId, videoIds) {
|
|
|
|
await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
|
|
|
|
method: "POST",
|
|
|
|
body: JSON.stringify({
|
|
|
|
playlistId: playlistId,
|
|
|
|
videoIds: videoIds,
|
|
|
|
}),
|
|
|
|
headers: {
|
|
|
|
Authorization: this.getAuthToken(),
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
2023-01-06 18:30:28 +00:00
|
|
|
async loadPlaylistBookmarks() {
|
|
|
|
if (!window.db) return;
|
|
|
|
var tx = window.db.transaction("playlist_bookmarks", "readonly");
|
|
|
|
var store = tx.objectStore("playlist_bookmarks");
|
|
|
|
const cursorRequest = store.openCursor();
|
|
|
|
cursorRequest.onsuccess = e => {
|
|
|
|
const cursor = e.target.result;
|
|
|
|
if (cursor) {
|
|
|
|
const bookmark = cursor.value;
|
|
|
|
this.bookmarks.push(bookmark);
|
|
|
|
cursor.continue();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
2023-01-06 19:38:42 +00:00
|
|
|
async removeBookmark(index) {
|
|
|
|
var tx = window.db.transaction("playlist_bookmarks", "readwrite");
|
|
|
|
var store = tx.objectStore("playlist_bookmarks");
|
|
|
|
store.delete(this.bookmarks[index].playlistId);
|
|
|
|
this.bookmarks.splice(index, 1);
|
|
|
|
},
|
2022-04-07 02:33:25 +00:00
|
|
|
},
|
2023-05-31 07:50:59 +00:00
|
|
|
components: { ConfirmModal, ModalComponent },
|
2022-04-07 02:33:25 +00:00
|
|
|
};
|
|
|
|
</script>
|