Piped/src/components/PlaylistsPage.vue

263 lines
11 KiB
Vue
Raw Normal View History

2022-04-07 02:33:25 +00:00
<template>
<h2 v-t="'titles.playlists'" class="my-4 font-bold" />
2022-04-07 02:33:25 +00:00
<div class="mb-3 flex justify-between">
<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" />
<input
id="fileSelector"
ref="fileSelector"
type="file"
class="hidden"
multiple="multiple"
2023-07-27 11:46:05 +00:00
@change="importPlaylists"
/>
<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" />
<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="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)" />
<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>
<ConfirmModal
v-if="playlistToDelete == playlist.id"
:message="$t('actions.delete_playlist_confirm')"
@close="playlistToDelete = null"
@confirm="onDeletePlaylist(playlist.id)"
/>
2022-04-07 02:33:25 +00:00
</div>
</div>
2023-01-06 19:38:42 +00:00
<hr />
<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')}`" />
<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"
class="link my-2 flex overflow-hidden"
2023-01-06 18:30:28 +00:00
:title="playlist.name"
v-text="playlist.name"
/>
<a :href="playlist.uploaderUrl" class="flex items-center">
<img class="h-32px w-32px rounded-full" :src="playlist.uploaderAvatar" />
<span class="ml-3 hover:underline" v-text="playlist.uploader" />
</a>
2023-01-06 18:30:28 +00:00
</router-link>
</div>
<br />
<CreatePlaylistModal
v-if="showCreatePlaylistModal"
@close="showCreatePlaylistModal = false"
@created="fetchPlaylists"
/>
2022-04-07 02:33:25 +00:00
</template>
<script>
import ConfirmModal from "./ConfirmModal.vue";
2023-05-30 12:24:59 +00:00
import ModalComponent from "./ModalComponent.vue";
import CreatePlaylistModal from "./CreatePlaylistModal.vue";
2022-04-07 02:33:25 +00:00
export default {
components: { ConfirmModal, ModalComponent, CreatePlaylistModal },
2022-04-07 02:33:25 +00:00
data() {
return {
playlists: [],
2023-01-06 18:30:28 +00:00
bookmarks: [],
playlistToDelete: null,
2023-05-30 12:24:59 +00:00
playlistToEdit: null,
newPlaylistName: "",
newPlaylistDescription: "",
showCreatePlaylistModal: false,
2022-04-07 02:33:25 +00:00
};
},
mounted() {
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() {
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) {
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) {
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
},
onDeletePlaylist(id) {
this.deletePlaylist(id).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-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) {
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() {
const files = this.$refs.fileSelector.files;
for (let file of files) {
await this.importPlaylistFile(file);
}
window.location.reload();
},
async importPlaylistFile(file) {
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");
// 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 = {
name: playlistName ?? new Date().toJSON(),
2023-01-01 18:57:00 +00:00
videos: lines
.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>