diff --git a/src/App.vue b/src/App.vue index 331a2065..4eec7018 100644 --- a/src/App.vue +++ b/src/App.vue @@ -55,7 +55,7 @@ export default { }); if ("indexedDB" in window) { - const request = indexedDB.open("piped-db", 4); + const request = indexedDB.open("piped-db", 5); request.onupgradeneeded = ev => { const db = request.result; console.log("Upgrading object store."); @@ -77,6 +77,12 @@ export default { const store = db.createObjectStore("channel_groups", { keyPath: "groupName" }); store.createIndex("groupName", "groupName", { unique: true }); } + if (!db.objectStoreNames.contains("playlists")) { + const playlistStore = db.createObjectStore("playlists", { keyPath: "playlistId" }); + playlistStore.createIndex("playlistId", "playlistId", { unique: true }); + const playlistVideosStore = db.createObjectStore("playlistVideos", { keyPath: "videoId" }); + playlistVideosStore.createIndex("videoId", "videoId", { unique: true }); + } }; request.onsuccess = e => { window.db = e.target.result; diff --git a/src/components/PlaylistPage.vue b/src/components/PlaylistPage.vue index b2ea2361..80ad5cb4 100644 --- a/src/components/PlaylistPage.vue +++ b/src/components/PlaylistPage.vue @@ -86,14 +86,11 @@ export default { mounted() { const playlistId = this.$route.query.list; if (this.authenticated && playlistId?.length == 36) - this.fetchJson(this.authApiUrl() + "/user/playlists", null, { - headers: { - Authorization: this.getAuthToken(), - }, - }).then(json => { + this.getPlaylists().then(json => { if (json.error) alert(json.error); else if (json.some(playlist => playlist.id === playlistId)) this.admin = true; }); + else if (playlistId.startsWith("local")) this.admin = true; this.isPlaylistBookmarked(); }, activated() { @@ -106,6 +103,11 @@ export default { }, methods: { async fetchPlaylist() { + const playlistId = this.$route.query.list; + if (playlistId.startsWith("local")) { + return this.getPlaylist(playlistId); + } + return await await this.fetchJson(this.authApiUrl() + "/playlists/" + this.$route.query.list); }, async getPlaylistData() { diff --git a/src/components/PlaylistsPage.vue b/src/components/PlaylistsPage.vue index 58c14543..06f4da30 100644 --- a/src/components/PlaylistsPage.vue +++ b/src/components/PlaylistsPage.vue @@ -238,8 +238,7 @@ export default { cursorRequest.onsuccess = e => { const cursor = e.target.result; if (cursor) { - const bookmark = cursor.value; - this.bookmarks.push(bookmark); + this.bookmarks.push(cursor.value); cursor.continue(); } }; diff --git a/src/components/VideoItem.vue b/src/components/VideoItem.vue index 702814e0..4b9a7dca 100644 --- a/src/components/VideoItem.vue +++ b/src/components/VideoItem.vue @@ -107,7 +107,7 @@ > - + diff --git a/src/components/WatchVideo.vue b/src/components/WatchVideo.vue index 6deff7f9..aaa34661 100644 --- a/src/components/WatchVideo.vue +++ b/src/components/WatchVideo.vue @@ -94,7 +94,7 @@ /> - + {{ $t("actions.add_to_playlist") }} @@ -494,7 +494,7 @@ export default { }, async fetchSubscribedStatus() { if (!this.channelId) return; - if (!this.authenticated) { + if ({ this.subscribed = this.isSubscribedLocally(this.channelId); return; } @@ -531,7 +531,7 @@ export default { }); }, subscribeHandler() { - if (this.authenticated) { + if { this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, { method: "POST", body: JSON.stringify({ diff --git a/src/main.js b/src/main.js index a1ffb6d3..a0385f4d 100644 --- a/src/main.js +++ b/src/main.js @@ -194,6 +194,9 @@ const mixin = { timeAgo(time) { return timeAgo.format(time); }, + async delay(millis) { + return await new Promise(r => setTimeout(r, millis)); + }, urlify(string) { if (!string) return ""; const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g; @@ -292,7 +295,84 @@ const mixin = { var store = tx.objectStore("channel_groups"); store.delete(groupName); }, + async getLocalPlaylist(playlistId) { + var tx = window.db.transaction("playlists", "readonly"); + var store = tx.objectStore("playlists"); + const req = store.openCursor(playlistId); + let playlist = null; + req.onsuccess = e => { + playlist = e.target.result.value; + }; + while (playlist == null) { + await this.delay(10); + } + playlist.videos = JSON.parse(playlist.videoIds).length; + return playlist; + }, + createOrUpdateLocalPlaylist(playlist) { + var tx = window.db.transaction("playlists", "readwrite"); + var store = tx.objectStore("playlists"); + store.put(playlist); + }, + // needs to handle both, streamInfo items and streams items + createLocalPlaylistVideo(videoId, videoInfo) { + if (videoInfo === null || videoId === null) return; + + var tx = window.db.transaction("playlistVideos", "readwrite"); + var store = tx.objectStore("playlistVideos"); + const video = { + videoId: videoId, + title: videoInfo.title, + type: "stream", + shortDescription: videoInfo.shortDescription ?? videoInfo.description, + url: `/watch?v=${videoId}`, + thumbnailUrl: videoInfo.thumbnailUrl, + uploaderVerified: videoInfo.uploaderVerified, + duration: videoInfo.duration, + uploaderAvatar: videoInfo.uploaderAvatar, + uploaderUrl: videoInfo.uploaderUrl, + uploaderName: videoInfo.uploaderName ?? videoInfo.uploader, + }; + store.put(video); + }, + async getLocalPlaylistVideo(videoId) { + var tx = window.db.transaction("playlistVideos", "readonly"); + var store = tx.objectStore("playlistVideos"); + const req = store.openCursor(videoId); + let video = null; + req.onsuccess = e => { + video = e.target.result; + }; + while (video == null) { + await this.delay(10); + } + return video; + }, async getPlaylists() { + if (!this.authenticated) { + if (!window.db) return []; + let finished = false; + let playlists = []; + var tx = window.db.transaction("playlists", "readonly"); + var store = tx.objectStore("playlists"); + const cursorRequest = store.openCursor(); + cursorRequest.onsuccess = e => { + const cursor = e.target.result; + if (cursor) { + let playlist = cursor.value; + playlist.videos = JSON.parse(playlist.videoIds).length; + playlists.push(playlist); + cursor.continue(); + } else { + finished = true; + } + }; + while (!finished) { + await this.delay(10); + } + return playlists; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists", null, { headers: { Authorization: this.getAuthToken(), @@ -300,9 +380,31 @@ const mixin = { }); }, async getPlaylist(playlistId) { + if (!this.authenticated) { + const playlist = await this.getLocalPlaylist(playlistId); + const videoIds = JSON.parse(playlist.videoIds); + const videosFuture = videoIds.map(videoId => this.getLocalPlaylistVideo(videoId)); + playlist.relatedStreams = Promise.all(videosFuture); + return playlist; + } + return await this.fetchJson(this.authApiUrl() + "/playlists/" + playlistId); }, async createPlaylist(name) { + if (!this.authenticated) { + const playlistId = "local-1"; + this.createOrUpdateLocalPlaylist({ + playlistId: playlistId, + // remapping needed for the playlists page + id: playlistId, + name: name, + description: "", + thumbnail: "https://pipedproxy.kavin.rocks/?host=i.ytimg.com", + videoIds: "[]", // empty list + }); + return { playlistId: playlistId }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/create", null, { method: "POST", body: JSON.stringify({ @@ -315,6 +417,13 @@ const mixin = { }); }, async deletePlaylist(playlistId) { + if (!this.authenticated) { + var tx = window.db.transaction("playlists", "readwrite"); + var store = tx.objectStore("playlists"); + store.delete(playlistId); + return { message: "ok" }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, { method: "POST", body: JSON.stringify({ @@ -327,6 +436,13 @@ const mixin = { }); }, async renamePlaylist(playlistId, newName) { + if (!this.authenticated) { + const playlist = await this.getLocalPlaylist(playlistId); + playlist.name = newName; + this.createOrUpdateLocalPlaylist(playlist); + return { message: "ok" }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, { method: "POST", body: JSON.stringify({ @@ -340,6 +456,13 @@ const mixin = { }); }, async changePlaylistDescription(playlistId, newDescription) { + if (!this.authenticated) { + const playlist = await this.getLocalPlaylist(playlistId); + playlist.description = newDescription; + this.createOrUpdateLocalPlaylist(playlist); + return { message: "ok" }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, { method: "PATCH", body: JSON.stringify({ @@ -353,7 +476,19 @@ const mixin = { }); }, async addVideosToPlaylist(playlistId, videoIds, videoInfos) { - if (videoInfos == "hallo") return; //TODO, only needed for local vids + if (!this.authenticated) { + const playlist = await this.getLocalPlaylist(playlistId); + const currentVideoIds = JSON.parse(playlist.videoIds); + if (currentVideoIds.length == 0) playlist.thumbnailUrl = videoInfos[0].thumbnailUrl; + videoIds.push(...videoIds); + playlist.videoIds = JSON.stringify(videoIds); + this.createOrUpdateLocalPlaylist(playlist); + for (let i in videoIds) { + this.createLocalPlaylistVideo(videoIds[i], videoInfos[i]); + } + return { message: "ok" }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, { method: "POST", body: JSON.stringify({ @@ -367,6 +502,16 @@ const mixin = { }); }, async removeVideoFromPlaylist(playlistId, videoId) { + if (!this.authenticated) { + const playlist = await this.getLocalPlaylist(playlistId); + const videoIds = JSON.parse(playlist.videoIds); + videoIds.splice(videoIds.indexOf(videoId), 1); + playlist.videoIds = JSON.stringify(videoIds); + if (videoIds.length == 0) playlist.thumbnailUrl = ""; + this.createOrUpdateLocalPlaylist(playlist); + return { message: "ok" }; + } + return await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, { method: "POST", body: JSON.stringify({