2022-04-07 02:33:25 +00:00
|
|
|
<template>
|
2023-08-13 17:31:57 +00:00
|
|
|
<h2 v-t="'titles.playlists'" class="my-4 font-bold" />
|
2022-04-07 02:33:25 +00:00
|
|
|
|
2023-08-13 17:31:57 +00:00
|
|
|
<div class="mb-3 flex justify-between">
|
2024-02-20 08:43:28 +00:00
|
|
|
<button v-t="'actions.create_playlist'" class="btn" @click="showCreatePlaylistModal = true" />
|
2022-11-28 13:13:36 +00:00
|
|
|
<div class="flex">
|
2023-07-27 11:46:05 +00:00
|
|
|
<button v-if="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"
|
2024-01-24 16:59:16 +00:00
|
|
|
class="hidden"
|
2023-05-17 07:55:49 +00:00
|
|
|
multiple="multiple"
|
2023-07-27 11:46:05 +00:00
|
|
|
@change="importPlaylists"
|
2023-05-17 07:55:49 +00:00
|
|
|
/>
|
2023-08-23 14:15:43 +00:00
|
|
|
<label v-t="'actions.import_from_json_csv'" for="fileSelector" 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"
|
2023-08-13 17:31:57 +00:00
|
|
|
class="link my-2 flex overflow-hidden"
|
2022-04-07 02:33:25 +00:00
|
|
|
:title="playlist.name"
|
|
|
|
v-text="playlist.name"
|
|
|
|
/>
|
|
|
|
</router-link>
|
2023-07-27 11:46:05 +00:00
|
|
|
<button v-t="'actions.edit_playlist'" class="btn h-auto" @click="showPlaylistEditModal(playlist)" />
|
2023-08-13 17:31:57 +00:00
|
|
|
<button v-t="'actions.delete_playlist'" class="btn ml-2 h-auto" @click="playlistToDelete = playlist.id" />
|
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
|
2023-07-27 11:46:05 +00:00
|
|
|
v-model="newPlaylistName"
|
2023-05-30 12:24:59 +00:00
|
|
|
class="input"
|
|
|
|
type="text"
|
|
|
|
:placeholder="$t('actions.playlist_name')"
|
|
|
|
/>
|
|
|
|
<input
|
2023-07-27 11:46:05 +00:00
|
|
|
v-model="newPlaylistDescription"
|
2023-05-30 12:24:59 +00:00
|
|
|
class="input"
|
|
|
|
type="text"
|
|
|
|
:placeholder="$t('actions.playlist_description')"
|
|
|
|
/>
|
2023-07-27 11:46:05 +00:00
|
|
|
<button v-t="'actions.okay'" class="btn ml-auto" @click="editPlaylist(playlist)" />
|
2023-05-30 12:24:59 +00:00
|
|
|
</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"
|
2023-06-15 15:32:32 +00:00
|
|
|
@confirm="onDeletePlaylist(playlist.id)"
|
2023-05-30 13:02:20 +00:00
|
|
|
/>
|
2022-04-07 02:33:25 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-01-06 19:38:42 +00:00
|
|
|
<hr />
|
|
|
|
|
2023-08-13 17:31:57 +00:00
|
|
|
<h2 v-t="'titles.bookmarks'" class="my-4 font-bold" />
|
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-08-13 17:31:57 +00:00
|
|
|
<div class="absolute bottom-100px right-5px z-100 px-5px" @click.prevent="removeBookmark(index)">
|
2024-03-06 19:45:31 +00:00
|
|
|
<i class="i-fa6-solid:bookmark ml-3" />
|
2023-01-06 19:38:42 +00:00
|
|
|
</div>
|
2023-01-06 18:30:28 +00:00
|
|
|
</div>
|
|
|
|
<p
|
|
|
|
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
|
2023-08-13 17:31:57 +00:00
|
|
|
class="link my-2 flex overflow-hidden"
|
2023-01-06 18:30:28 +00:00
|
|
|
:title="playlist.name"
|
|
|
|
v-text="playlist.name"
|
|
|
|
/>
|
2023-01-06 20:05:05 +00:00
|
|
|
<a :href="playlist.uploaderUrl" class="flex items-center">
|
2023-08-13 17:31:57 +00:00
|
|
|
<img class="h-32px w-32px rounded-full" :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 />
|
2024-02-20 08:43:28 +00:00
|
|
|
<CreatePlaylistModal
|
|
|
|
v-if="showCreatePlaylistModal"
|
|
|
|
@close="showCreatePlaylistModal = false"
|
|
|
|
@created="fetchPlaylists"
|
|
|
|
/>
|
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";
|
2024-02-20 08:43:28 +00:00
|
|
|
import CreatePlaylistModal from "./CreatePlaylistModal.vue";
|
2023-05-30 13:02:20 +00:00
|
|
|
|
2022-04-07 02:33:25 +00:00
|
|
|
export default {
|
2024-02-20 08:43:28 +00:00
|
|
|
components: { ConfirmModal, ModalComponent, CreatePlaylistModal },
|
2022-04-07 02:33:25 +00:00
|
|
|
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: "",
|
2024-02-20 08:43:28 +00:00
|
|
|
showCreatePlaylistModal: false,
|
2022-04-07 02:33:25 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
mounted() {
|
2023-06-15 15:32:32 +00:00
|
|
|
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() {
|
2023-06-15 15:32:32 +00:00
|
|
|
this.getPlaylists().then(json => {
|
2022-04-07 02:33:25 +00:00
|
|
|
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) {
|
2023-06-15 15:32:32 +00:00
|
|
|
this.renamePlaylist(selectedPlaylist.id, newName).then(json => {
|
2023-05-30 12:24:59 +00:00
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else selectedPlaylist.name = newName;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (newDescription != selectedPlaylist.description) {
|
2023-06-15 15:32:32 +00:00
|
|
|
this.changePlaylistDescription(selectedPlaylist.id, newDescription).then(json => {
|
2023-05-30 12:24:59 +00:00
|
|
|
if (json.error) alert(json.error);
|
|
|
|
else selectedPlaylist.description = newDescription;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.playlistToEdit = null;
|
2022-08-28 17:58:33 +00:00
|
|
|
},
|
2023-06-15 15:32:32 +00:00
|
|
|
onDeletePlaylist(id) {
|
|
|
|
this.deletePlaylist(id).then(json => {
|
2023-05-30 13:02:20 +00:00
|
|
|
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-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) {
|
2023-06-15 15:32:32 +00:00
|
|
|
let playlist = await this.getPlaylist(playlistId);
|
|
|
|
return {
|
2022-11-28 13:13:36 +00:00
|
|
|
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
|
|
|
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) {
|
2024-06-14 22:33:57 +00:00
|
|
|
let text = (await file.text()).trim();
|
2023-01-01 18:57:00 +00:00
|
|
|
let tasks = [];
|
|
|
|
// list of playlists exported from Piped
|
2023-07-28 17:12:37 +00:00
|
|
|
if (file.name.slice(-4).toLowerCase() == ".csv") {
|
2023-01-01 18:57:00 +00:00
|
|
|
const lines = text.split("\n");
|
2024-06-14 22:33:57 +00:00
|
|
|
|
|
|
|
// old format: first two lines contain playlist info (e.g. name) in CSV format
|
|
|
|
// new format: no information about playlist like name, ...
|
|
|
|
// video list has two columns: videoId and date of addition
|
|
|
|
const playlistInfo = lines[1].split(",");
|
|
|
|
let videoListStartIndex = 0;
|
|
|
|
let playlistName = null;
|
|
|
|
if (playlistInfo.length > 2) {
|
|
|
|
playlistName = playlistInfo[4];
|
|
|
|
videoListStartIndex = 4;
|
|
|
|
}
|
|
|
|
|
2023-01-01 18:57:00 +00:00
|
|
|
const playlist = {
|
2024-06-14 22:33:57 +00:00
|
|
|
name: playlistName ?? new Date().toJSON(),
|
2023-01-01 18:57:00 +00:00
|
|
|
videos: lines
|
2024-06-14 22:33:57 +00:00
|
|
|
.slice(videoListStartIndex, lines.length)
|
2023-01-01 18:57:00 +00:00
|
|
|
.filter(line => line != "")
|
2023-07-28 17:12:37 +00:00
|
|
|
.slice(1)
|
2023-01-01 18:57:00 +00:00
|
|
|
.map(line => `https://youtube.com/watch?v=${line.split(",")[0]}`),
|
|
|
|
};
|
|
|
|
tasks.push(this.createPlaylistWithVideos(playlist));
|
2023-07-28 17:12:37 +00:00
|
|
|
} else if (text.includes('"Piped"')) {
|
|
|
|
// CSV from Google Takeout
|
|
|
|
let playlists = JSON.parse(text).playlists;
|
|
|
|
if (!playlists.length) {
|
|
|
|
alert(this.$t("actions.no_valid_playlists"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (let playlist of playlists) {
|
|
|
|
tasks.push(this.createPlaylistWithVideos(playlist));
|
|
|
|
}
|
2023-01-01 18:57:00 +00:00
|
|
|
} 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);
|
|
|
|
},
|
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) {
|
2023-06-15 17:18:47 +00:00
|
|
|
this.bookmarks.push(cursor.value);
|
2023-01-06 18:30:28 +00:00
|
|
|
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
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|