Skip to content

Commit

Permalink
feat: api support for playlist bookmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Jun 13, 2024
1 parent d67e50b commit d2057d9
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 15 deletions.
2 changes: 1 addition & 1 deletion config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ MATRIX_SERVER:https://matrix-client.matrix.org
#S3_BUCKET:INSERT_HERE

# Hibernate properties
hibernate.connection.url:jdbc:postgresql://postgres:5432/piped
hibernate.connection.url:jdbc:postgresql://localhost:5432/piped
hibernate.connection.driver_class:org.postgresql.Driver
hibernate.dialect:org.hibernate.dialect.PostgreSQLDialect
hibernate.connection.username:piped
Expand Down
11 changes: 2 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
services:
piped:
image: 1337kavin/piped:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./config.properties:/app/config.properties
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- ./data/db:/var/lib/postgresql/data
ports:
- 5432:5432
environment:
- POSTGRES_DB=piped
- POSTGRES_USER=piped
Expand Down
33 changes: 29 additions & 4 deletions src/main/java/me/kavin/piped/server/ServerLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.kavin.piped.consts.Constants;
import me.kavin.piped.server.handlers.*;
import me.kavin.piped.server.handlers.auth.AuthPlaylistHandlers;
import me.kavin.piped.server.handlers.auth.FeedHandlers;
import me.kavin.piped.server.handlers.auth.StorageHandlers;
import me.kavin.piped.server.handlers.auth.UserHandlers;
import me.kavin.piped.server.handlers.auth.*;
import me.kavin.piped.utils.*;
import me.kavin.piped.utils.resp.*;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -453,6 +450,34 @@ AsyncServlet mainServlet(Executor executor) {
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(POST, "/user/bookmarks/create", AsyncServlet.ofBlocking(executor, request -> {
try {
var playlistId = mapper.readTree(request.loadBody().getResult().asArray()).get("playlistId").textValue();
return getJsonResponse(PlaylistBookmarkHandlers.createPlaylistBookmarkResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(GET, "/user/bookmarks", AsyncServlet.ofBlocking(executor, request -> {
try {
return getJsonResponse(PlaylistBookmarkHandlers.playlistBookmarksResponse(request.getHeader(AUTHORIZATION)), "private");
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(GET, "/user/bookmarks/bookmarked", AsyncServlet.ofBlocking(executor, request -> {
try {
return getJsonResponse(PlaylistBookmarkHandlers.isBookmarkedResponse(request.getHeader(AUTHORIZATION),
request.getQueryParameter("playlistId")), "private");
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(POST, "/user/bookmarks/delete", AsyncServlet.ofBlocking(executor, request -> {
try {
var json = mapper.readTree(request.loadBody().getResult().asArray());
var playlistId = json.get("playlistId").textValue();
return getJsonResponse(PlaylistBookmarkHandlers.deletePlaylistBookmarkResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
} catch (Exception e) {
return getErrorResponse(e, request.getPath());
}
})).map(GET, "/registered/badge", AsyncServlet.ofBlocking(executor, request -> {
try {
return HttpResponse.ofCode(302).withHeader(LOCATION, GenericHandlers.registeredBadgeRedirect())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package me.kavin.piped.server.handlers.auth;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.kavin.piped.utils.DatabaseHelper;
import me.kavin.piped.utils.DatabaseSessionFactory;
import me.kavin.piped.utils.ExceptionHandler;
import me.kavin.piped.utils.obj.db.PlaylistBookmark;
import me.kavin.piped.utils.obj.db.User;
import me.kavin.piped.utils.resp.AcceptedResponse;
import me.kavin.piped.utils.resp.AuthenticationFailureResponse;
import me.kavin.piped.utils.resp.BookmarkedStatusResponse;
import me.kavin.piped.utils.resp.InvalidRequestResponse;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.Session;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;

import java.io.IOException;

import static me.kavin.piped.consts.Constants.mapper;
import static me.kavin.piped.utils.URLUtils.*;

public class PlaylistBookmarkHandlers {
public static byte[] createPlaylistBookmarkResponse(String session, String playlistId) throws IOException, ExtractionException {

if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and name are required parameters"));

User user = DatabaseHelper.getUserFromSession(session);

if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());

try (Session s = DatabaseSessionFactory.createSession()) {
if (DatabaseHelper.isBookmarked(s, user, playlistId)) {
var bookmark = DatabaseHelper.getPlaylistBookmarkFromPlaylistId(s, user, playlistId);
return mapper.writeValueAsBytes(createPlaylistBookmarkResponseItem(bookmark));
}

final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId);

var playlistBookmark = new PlaylistBookmark(playlistId, info.getName(), info.getDescription().getContent(), getLastThumbnail(info.getThumbnails()), info.getUploaderName(), substringYouTube(info.getUploaderUrl()), getLastThumbnail(info.getUploaderAvatars()), info.getStreamCount(), user);

var tr = s.beginTransaction();
s.persist(playlistBookmark);
tr.commit();

ObjectNode response = createPlaylistBookmarkResponseItem(playlistBookmark);

return mapper.writeValueAsBytes(response);
}
}

public static byte[] deletePlaylistBookmarkResponse(String session, String playlistId) throws IOException {

if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and playlistId are required parameters"));

User user = DatabaseHelper.getUserFromSession(session);

if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());

try (Session s = DatabaseSessionFactory.createSession()) {

DatabaseHelper.deletePlaylistBookmark(s, user, playlistId);

return mapper.writeValueAsBytes(new AcceptedResponse());
}
}

public static byte[] playlistBookmarksResponse(String session) throws IOException {

if (StringUtils.isBlank(session))
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session is a required parameter"));

User user = DatabaseHelper.getUserFromSession(session);

if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());

try (Session s = DatabaseSessionFactory.createSession()) {

var responseArray = new ObjectArrayList<>();
var playlistBookmarks = DatabaseHelper.getPlaylistBookmarks(s, user);

for (PlaylistBookmark bookmark : playlistBookmarks) {
responseArray.add(createPlaylistBookmarkResponseItem(bookmark));
}

return mapper.writeValueAsBytes(responseArray);
}
}

public static byte[] isBookmarkedResponse(String session, String playlistId) throws IOException {

if (StringUtils.isBlank(session) || StringUtils.isBlank(playlistId))
ExceptionHandler.throwErrorResponse(new InvalidRequestResponse("session and playlistId are required parameters"));

User user = DatabaseHelper.getUserFromSession(session);

if (user == null) ExceptionHandler.throwErrorResponse(new AuthenticationFailureResponse());

try (Session s = DatabaseSessionFactory.createSession()) {
boolean isBookmarked = DatabaseHelper.isBookmarked(s, user, playlistId);

return mapper.writeValueAsBytes(new BookmarkedStatusResponse(isBookmarked));
}
}

private static ObjectNode createPlaylistBookmarkResponseItem(PlaylistBookmark bookmark) {
ObjectNode node = mapper.createObjectNode();
node.put("playlistId", String.valueOf(bookmark.getPlaylistId()));
node.put("name", bookmark.getName());
node.put("shortDescription", bookmark.getShortDescription());
node.put("thumbnailUrl", rewriteURL(bookmark.getThumbnailUrl()));
node.put("uploader", bookmark.getUploader());
node.put("uploaderUrl", bookmark.getUploaderUrl());
node.put("uploaderAvatar", rewriteURL(bookmark.getUploaderAvatar()));
node.put("videos", bookmark.getVideoCount());
return node;
}
}
45 changes: 45 additions & 0 deletions src/main/java/me/kavin/piped/utils/DatabaseHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import jakarta.persistence.criteria.Root;
import me.kavin.piped.consts.Constants;
import me.kavin.piped.utils.obj.db.*;
import me.kavin.piped.utils.resp.InvalidRequestResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.Session;
import org.hibernate.SharedSessionContract;
import org.hibernate.StatelessSession;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
Expand Down Expand Up @@ -236,4 +238,47 @@ public static Channel saveChannel(String channelId) {

return channel;
}

public static List<PlaylistBookmark> getPlaylistBookmarks(SharedSessionContract s, User user) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<PlaylistBookmark> cr = cb.createQuery(PlaylistBookmark.class);
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
cr.select(root).where(cb.equal(root.get("owner"), user));

return s.createQuery(cr).getResultList();
}

public static boolean isBookmarked(SharedSessionContract s, User user, String playlistId) {
CriteriaBuilder cb = s.getCriteriaBuilder();

CriteriaQuery<Long> cr = cb.createQuery(Long.class);
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
cr.select(cb.count(root)).where(cb.and(
cb.equal(root.get("owner"), user)),
cb.equal(root.get("playlist_id"), playlistId)
);

return s.createQuery(cr).getSingleResult() > 0;
}

public static PlaylistBookmark getPlaylistBookmarkFromPlaylistId(SharedSessionContract s, User user, String playlistId) {
CriteriaBuilder cb = s.getCriteriaBuilder();

CriteriaQuery<PlaylistBookmark> cr = cb.createQuery(PlaylistBookmark.class);
Root<PlaylistBookmark> root = cr.from(PlaylistBookmark.class);
cr.select(root).where(cb.and(
cb.equal(root.get("owner"), user)),
cb.equal(root.get("playlist_id"), playlistId)
);

return s.createQuery(cr).uniqueResult();
}

public static void deletePlaylistBookmark(Session s, User user, String playlistId) {
var playlistBookmark = DatabaseHelper.getPlaylistBookmarkFromPlaylistId(s, user, playlistId);

var tr = s.beginTransaction();
s.remove(playlistBookmark);
tr.commit();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class DatabaseSessionFactory {

sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class)
.addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class)
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory();
.addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class)
.addAnnotatedClass(PlaylistBookmark.class).buildSessionFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand Down
Loading

0 comments on commit d2057d9

Please sign in to comment.