mirror of
https://github.com/TeamPiped/Piped.git
synced 2025-01-11 07:17:00 +00:00
Add support for local playlists
This commit is contained in:
parent
2e64c4f003
commit
37d6423e08
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
147
src/main.js
147
src/main.js
@ -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({
|
||||||
|
Loading…
Reference in New Issue
Block a user