Add support for local playlists

This commit is contained in:
Bnyro 2023-06-15 19:18:47 +02:00
parent 2e64c4f003
commit 37d6423e08
6 changed files with 167 additions and 15 deletions

View File

@ -55,7 +55,7 @@ export default {
}); });
if ("indexedDB" in window) { if ("indexedDB" in window) {
const request = indexedDB.open("piped-db", 4); const request = indexedDB.open("piped-db", 5);
request.onupgradeneeded = ev => { request.onupgradeneeded = ev => {
const db = request.result; const db = request.result;
console.log("Upgrading object store."); console.log("Upgrading object store.");
@ -77,6 +77,12 @@ export default {
const store = db.createObjectStore("channel_groups", { keyPath: "groupName" }); const store = db.createObjectStore("channel_groups", { keyPath: "groupName" });
store.createIndex("groupName", "groupName", { unique: true }); 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 => { request.onsuccess = e => {
window.db = e.target.result; window.db = e.target.result;

View File

@ -86,14 +86,11 @@ export default {
mounted() { mounted() {
const playlistId = this.$route.query.list; const playlistId = this.$route.query.list;
if (this.authenticated && playlistId?.length == 36) if (this.authenticated && playlistId?.length == 36)
this.fetchJson(this.authApiUrl() + "/user/playlists", null, { this.getPlaylists().then(json => {
headers: {
Authorization: this.getAuthToken(),
},
}).then(json => {
if (json.error) alert(json.error); if (json.error) alert(json.error);
else if (json.some(playlist => playlist.id === playlistId)) this.admin = true; else if (json.some(playlist => playlist.id === playlistId)) this.admin = true;
}); });
else if (playlistId.startsWith("local")) this.admin = true;
this.isPlaylistBookmarked(); this.isPlaylistBookmarked();
}, },
activated() { activated() {
@ -106,6 +103,11 @@ export default {
}, },
methods: { methods: {
async fetchPlaylist() { 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); return await await this.fetchJson(this.authApiUrl() + "/playlists/" + this.$route.query.list);
}, },
async getPlaylistData() { async getPlaylistData() {

View File

@ -238,8 +238,7 @@ export default {
cursorRequest.onsuccess = e => { cursorRequest.onsuccess = e => {
const cursor = e.target.result; const cursor = e.target.result;
if (cursor) { if (cursor) {
const bookmark = cursor.value; this.bookmarks.push(cursor.value);
this.bookmarks.push(bookmark);
cursor.continue(); cursor.continue();
} }
}; };

View File

@ -107,7 +107,7 @@
> >
<font-awesome-icon icon="headphones" /> <font-awesome-icon icon="headphones" />
</router-link> </router-link>
<button v-if="authenticated" :title="$t('actions.add_to_playlist')" @click="showModal = !showModal"> <button :title="$t('actions.add_to_playlist')" @click="showModal = !showModal">
<font-awesome-icon icon="circle-plus" /> <font-awesome-icon icon="circle-plus" />
</button> </button>
<button <button
@ -127,7 +127,7 @@
<PlaylistAddModal <PlaylistAddModal
v-if="showModal" v-if="showModal"
:video-id="item.url.substr(-11)" :video-id="item.url.substr(-11)"
video-info="item" :video-info="item"
@close="showModal = !showModal" @close="showModal = !showModal"
/> />
</div> </div>

View File

@ -94,7 +94,7 @@
/> />
<div class="flex flex-wrap gap-1 ml-auto"> <div class="flex flex-wrap gap-1 ml-auto">
<!-- Subscribe Button --> <!-- Subscribe Button -->
<button class="btn flex items-center" v-if="authenticated" @click="showModal = !showModal"> <button class="btn flex items-center" @click="showModal = !showModal">
{{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" /> {{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" />
</button> </button>
<button <button
@ -112,7 +112,7 @@
title="RSS feed" title="RSS feed"
role="button" role="button"
v-if="video.uploaderUrl" v-if="video.uploaderUrl"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`" :href="`${apiUrl()}/fss?channels=${video.uploaderUrl.split('/')[2]}`"
target="_blank" target="_blank"
class="btn flex items-center" class="btn flex items-center"
> >
@ -494,7 +494,7 @@ export default {
}, },
async fetchSubscribedStatus() { async fetchSubscribedStatus() {
if (!this.channelId) return; if (!this.channelId) return;
if (!this.authenticated) { if ({
this.subscribed = this.isSubscribedLocally(this.channelId); this.subscribed = this.isSubscribedLocally(this.channelId);
return; return;
} }
@ -531,7 +531,7 @@ export default {
}); });
}, },
subscribeHandler() { subscribeHandler() {
if (this.authenticated) { if {
this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, { this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({

View File

@ -194,6 +194,9 @@ const mixin = {
timeAgo(time) { timeAgo(time) {
return timeAgo.format(time); return timeAgo.format(time);
}, },
async delay(millis) {
return await new Promise(r => setTimeout(r, millis));
},
urlify(string) { urlify(string) {
if (!string) return ""; if (!string) return "";
const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g; const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
@ -292,7 +295,84 @@ const mixin = {
var store = tx.objectStore("channel_groups"); var store = tx.objectStore("channel_groups");
store.delete(groupName); 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() { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
headers: { headers: {
Authorization: this.getAuthToken(), Authorization: this.getAuthToken(),
@ -300,9 +380,31 @@ const mixin = {
}); });
}, },
async getPlaylist(playlistId) { 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); return await this.fetchJson(this.authApiUrl() + "/playlists/" + playlistId);
}, },
async createPlaylist(name) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/create", null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -315,6 +417,13 @@ const mixin = {
}); });
}, },
async deletePlaylist(playlistId) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -327,6 +436,13 @@ const mixin = {
}); });
}, },
async renamePlaylist(playlistId, newName) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -340,6 +456,13 @@ const mixin = {
}); });
}, },
async changePlaylistDescription(playlistId, newDescription) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ body: JSON.stringify({
@ -353,7 +476,19 @@ const mixin = {
}); });
}, },
async addVideosToPlaylist(playlistId, videoIds, videoInfos) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
@ -367,6 +502,16 @@ const mixin = {
}); });
}, },
async removeVideoFromPlaylist(playlistId, videoId) { 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, { return await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({