Skip to content

Commit d2057d9

Browse files
committed
feat: api support for playlist bookmarks
1 parent d67e50b commit d2057d9

11 files changed

Lines changed: 398 additions & 15 deletions

File tree

config.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ MATRIX_SERVER:https://matrix-client.matrix.org
8080
#S3_BUCKET:INSERT_HERE
8181

8282
# Hibernate properties
83-
hibernate.connection.url:jdbc:postgresql://postgres:5432/piped
83+
hibernate.connection.url:jdbc:postgresql://localhost:5432/piped
8484
hibernate.connection.driver_class:org.postgresql.Driver
8585
hibernate.dialect:org.hibernate.dialect.PostgreSQLDialect
8686
hibernate.connection.username:piped

docker-compose.yml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
services:
2-
piped:
3-
image: 1337kavin/piped:latest
4-
restart: unless-stopped
5-
ports:
6-
- "127.0.0.1:8080:8080"
7-
volumes:
8-
- ./config.properties:/app/config.properties
9-
depends_on:
10-
- postgres
112
postgres:
123
image: postgres:16-alpine
134
restart: unless-stopped
145
volumes:
156
- ./data/db:/var/lib/postgresql/data
7+
ports:
8+
- 5432:5432
169
environment:
1710
- POSTGRES_DB=piped
1811
- POSTGRES_USER=piped

src/main/java/me/kavin/piped/server/ServerLauncher.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
1212
import me.kavin.piped.consts.Constants;
1313
import me.kavin.piped.server.handlers.*;
14-
import me.kavin.piped.server.handlers.auth.AuthPlaylistHandlers;
15-
import me.kavin.piped.server.handlers.auth.FeedHandlers;
16-
import me.kavin.piped.server.handlers.auth.StorageHandlers;
17-
import me.kavin.piped.server.handlers.auth.UserHandlers;
14+
import me.kavin.piped.server.handlers.auth.*;
1815
import me.kavin.piped.utils.*;
1916
import me.kavin.piped.utils.resp.*;
2017
import org.apache.commons.lang3.StringUtils;
@@ -453,6 +450,34 @@ AsyncServlet mainServlet(Executor executor) {
453450
} catch (Exception e) {
454451
return getErrorResponse(e, request.getPath());
455452
}
453+
})).map(POST, "/user/bookmarks/create", AsyncServlet.ofBlocking(executor, request -> {
454+
try {
455+
var playlistId = mapper.readTree(request.loadBody().getResult().asArray()).get("playlistId").textValue();
456+
return getJsonResponse(PlaylistBookmarkHandlers.createPlaylistBookmarkResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
457+
} catch (Exception e) {
458+
return getErrorResponse(e, request.getPath());
459+
}
460+
})).map(GET, "/user/bookmarks", AsyncServlet.ofBlocking(executor, request -> {
461+
try {
462+
return getJsonResponse(PlaylistBookmarkHandlers.playlistBookmarksResponse(request.getHeader(AUTHORIZATION)), "private");
463+
} catch (Exception e) {
464+
return getErrorResponse(e, request.getPath());
465+
}
466+
})).map(GET, "/user/bookmarks/bookmarked", AsyncServlet.ofBlocking(executor, request -> {
467+
try {
468+
return getJsonResponse(PlaylistBookmarkHandlers.isBookmarkedResponse(request.getHeader(AUTHORIZATION),
469+
request.getQueryParameter("playlistId")), "private");
470+
} catch (Exception e) {
471+
return getErrorResponse(e, request.getPath());
472+
}
473+
})).map(POST, "/user/bookmarks/delete", AsyncServlet.ofBlocking(executor, request -> {
474+
try {
475+
var json = mapper.readTree(request.loadBody().getResult().asArray());
476+
var playlistId = json.get("playlistId").textValue();
477+
return getJsonResponse(PlaylistBookmarkHandlers.deletePlaylistBookmarkResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
478+
} catch (Exception e) {
479+
return getErrorResponse(e, request.getPath());
480+
}
456481
})).map(GET, "/registered/badge", AsyncServlet.ofBlocking(executor, request -> {
457482
try {
458483
return HttpResponse.ofCode(302).withHeader(LOCATION, GenericHandlers.registeredBadgeRedirect())
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package me.kavin.piped.server.handlers.auth;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.node.ObjectNode;
5+
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
6+
import me.kavin.piped.utils.DatabaseHelper;
7+
import me.kavin.piped.utils.DatabaseSessionFactory;
8+
import me.kavin.piped.utils.ExceptionHandler;
9+
import me.kavin.piped.utils.obj.db.PlaylistBookmark;
10+
import me.kavin.piped.utils.obj.db.User;
11+
import me.kavin.piped.utils.resp.AcceptedResponse;
12+
import me.kavin.piped.utils.resp.AuthenticationFailureResponse;
13+
import me.kavin.piped.utils.resp.BookmarkedStatusResponse;
14+
import me.kavin.piped.utils.resp.InvalidRequestResponse;
15+
import org.apache.commons.lang3.StringUtils;
16+
import org.hibernate.Session;
17+
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
18+
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
19+
20+
import java.io.IOException;
21+
22+
import static me.kavin.piped.consts.Constants.mapper;
23+
import static me.kavin.piped.utils.URLUtils.*;
24+
25+
public class PlaylistBookmarkHandlers {
26+
public static byte[] createPlaylistBookmarkResponse(String session, String playlistId) throws IOException, ExtractionException {
27+
28+
if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
29+
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and name are required parameters"));
30+
31+
User user = DatabaseHelper.getUserFromSession(session);
32+
33+
if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());
34+
35+
try (Session s = DatabaseSessionFactory.createSession()) {
36+
if (DatabaseHelper.isBookmarked(s, user, playlistId)) {
37+
var bookmark = DatabaseHelper.getPlaylistBookmarkFromPlaylistId(s, user, playlistId);
38+
return mapper.writeValueAsBytes(createPlaylistBookmarkResponseItem(bookmark));
39+
}
40+
41+
final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId);
42+
43+
var playlistBookmark = new PlaylistBookmark(playlistId, info.getName(), info.getDescription().getContent(), getLastThumbnail(info.getThumbnails()), info.getUploaderName(), substringYouTube(info.getUploaderUrl()), getLastThumbnail(info.getUploaderAvatars()), info.getStreamCount(), user);
44+
45+
var tr = s.beginTransaction();
46+
s.persist(playlistBookmark);
47+
tr.commit();
48+
49+
ObjectNode response = createPlaylistBookmarkResponseItem(playlistBookmark);
50+
51+
return mapper.writeValueAsBytes(response);
52+
}
53+
}
54+
55+
public static byte[] deletePlaylistBookmarkResponse(String session, String playlistId) throws IOException {
56+
57+
if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
58+
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and playlistId are required parameters"));
59+
60+
User user = DatabaseHelper.getUserFromSession(session);
61+
62+
if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());
63+
64+
try (Session s = DatabaseSessionFactory.createSession()) {
65+
66+
DatabaseHelper.deletePlaylistBookmark(s, user, playlistId);
67+
68+
return mapper.writeValueAsBytes(new AcceptedResponse());
69+
}
70+
}
71+
72+
public static byte[] playlistBookmarksResponse(String session) throws IOException {
73+
74+
if (StringUtils.isBlank(session))
75+
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session is a required parameter"));
76+
77+
User user = DatabaseHelper.getUserFromSession(session);
78+
79+
if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());
80+
81+
try (Session s = DatabaseSessionFactory.createSession()) {
82+
83+
var responseArray = new ObjectArrayList<>();
84+
var playlistBookmarks = DatabaseHelper.getPlaylistBookmarks(s, user);
85+
86+
for (PlaylistBookmark bookmark : playlistBookmarks) {
87+
responseArray.add(createPlaylistBookmarkResponseItem(bookmark));
88+
}
89+
90+
return mapper.writeValueAsBytes(responseArray);
91+
}
92+
}
93+
94+
public static byte[] isBookmarkedResponse(String session, String playlistId) throws IOException {
95+
96+
if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
97+
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and playlistId are required parameters"));
98+
99+
User user = DatabaseHelper.getUserFromSession(session);
100+
101+
if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());
102+
103+
try (Session s = DatabaseSessionFactory.createSession()) {
104+
boolean isBookmarked = DatabaseHelper.isBookmarked(s, user, playlistId);
105+
106+
return mapper.writeValueAsBytes(new BookmarkedStatusResponse(isBookmarked));
107+
}
108+
}
109+
110+
private static ObjectNode createPlaylistBookmarkResponseItem(PlaylistBookmark bookmark) {
111+
ObjectNode node = mapper.createObjectNode();
112+
node.put("playlistId", String.valueOf(bookmark.getPlaylistId()));
113+
node.put("name", bookmark.getName());
114+
node.put("shortDescription", bookmark.getShortDescription());
115+
node.put("thumbnailUrl", rewriteURL(bookmark.getThumbnailUrl()));
116+
node.put("uploader", bookmark.getUploader());
117+
node.put("uploaderUrl", bookmark.getUploaderUrl());
118+
node.put("uploaderAvatar", rewriteURL(bookmark.getUploaderAvatar()));
119+
node.put("videos", bookmark.getVideoCount());
120+
return node;
121+
}
122+
}

src/main/java/me/kavin/piped/utils/DatabaseHelper.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import jakarta.persistence.criteria.Root;
77
import me.kavin.piped.consts.Constants;
88
import me.kavin.piped.utils.obj.db.*;
9+
import me.kavin.piped.utils.resp.InvalidRequestResponse;
910
import org.apache.commons.lang3.StringUtils;
1011
import org.apache.commons.lang3.exception.ExceptionUtils;
12+
import org.hibernate.Session;
1113
import org.hibernate.SharedSessionContract;
1214
import org.hibernate.StatelessSession;
1315
import org.schabi.newpipe.extractor.channel.ChannelInfo;
@@ -236,4 +238,47 @@ public static Channel saveChannel(String channelId) {
236238

237239
return channel;
238240
}
241+
242+
public static List<PlaylistBookmark> getPlaylistBookmarks(SharedSessionContract s, User user) {
243+
CriteriaBuilder cb = s.getCriteriaBuilder();
244+
CriteriaQuery<PlaylistBookmark> cr = cb.createQuery(PlaylistBookmark.class);
245+
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
246+
cr.select(root).where(cb.equal(root.get("owner"), user));
247+
248+
return s.createQuery(cr).getResultList();
249+
}
250+
251+
public static boolean isBookmarked(SharedSessionContract s, User user, String playlistId) {
252+
CriteriaBuilder cb = s.getCriteriaBuilder();
253+
254+
CriteriaQuery<Long> cr = cb.createQuery(Long.class);
255+
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
256+
cr.select(cb.count(root)).where(cb.and(
257+
cb.equal(root.get("owner"), user)),
258+
cb.equal(root.get("playlist_id"), playlistId)
259+
);
260+
261+
return s.createQuery(cr).getSingleResult() > 0;
262+
}
263+
264+
public static PlaylistBookmark getPlaylistBookmarkFromPlaylistId(SharedSessionContract s, User user, String playlistId) {
265+
CriteriaBuilder cb = s.getCriteriaBuilder();
266+
267+
CriteriaQuery<PlaylistBookmark> cr = cb.createQuery(PlaylistBookmark.class);
268+
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
269+
cr.select(root).where(cb.and(
270+
cb.equal(root.get("owner"), user)),
271+
cb.equal(root.get("playlist_id"), playlistId)
272+
);
273+
274+
return s.createQuery(cr).uniqueResult();
275+
}
276+
277+
public static void deletePlaylistBookmark(Session s, User user, String playlistId) {
278+
var playlistBookmark = DatabaseHelper.getPlaylistBookmarkFromPlaylistId(s, user, playlistId);
279+
280+
var tr = s.beginTransaction();
281+
s.remove(playlistBookmark);
282+
tr.commit();
283+
}
239284
}

src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public class DatabaseSessionFactory {
2020

2121
sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class)
2222
.addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class)
23-
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory();
23+
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class)
24+
.addAnnotatedClass(PlaylistBookmark.class).buildSessionFactory();
2425
} catch (Exception e) {
2526
throw new RuntimeException(e);
2627
}

0 commit comments

Comments
 (0)