Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: api support for playlist bookmarks #815

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
135 changes: 135 additions & 0 deletions src/main/java/me/kavin/piped/utils/obj/db/PlaylistBookmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package me.kavin.piped.utils.obj.db;

import jakarta.persistence.*;

@Entity
@Table(name = "playlist_bookmarks", indexes = {@Index(columnList = "playlist_id", name = "playlist_bookmarks_playlist_id_idx"), @Index(columnList = "owner", name = "playlist_bookmarks_owner_idx")})
public class PlaylistBookmark {

public PlaylistBookmark() {
}

public PlaylistBookmark(String playlist_id, String name, String short_description, String thumbnail_url, String uploader, String uploader_url, String uploader_avatar, long video_count, User owner) {
this.playlist_id = playlist_id;
this.name = name;
this.short_description = short_description;
this.thumbnail_url = thumbnail_url;
this.uploader = uploader;
this.uploader_url = uploader_url;
this.uploader_avatar = uploader_avatar;
this.video_count = video_count;
this.owner = owner;
}

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(name = "playlist_id", nullable = false)
private String playlist_id;

@Column(name = "name", length = 200)
private String name;

@Column(name = "short_description", length = 300)
private String short_description;

@Column(name = "thumbnail_url", length = 300)
private String thumbnail_url;

@Column(name = "uploader", length = 100)
private String uploader;

@Column(name = "uploader_url", length = 100)
private String uploader_url;

@Column(name = "uploader_avatar", length = 150)
private String uploader_avatar;

@Column(name = "video_count")
private long video_count;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner", nullable = false)
private User owner;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getPlaylistId() {
return playlist_id;
}

public void setPlaylistId(String playlist_id) {
this.playlist_id = playlist_id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getShortDescription() {
return short_description;
}

public void setShortDescription(String short_description) {
this.short_description = short_description;
}

public String getThumbnailUrl() {
return thumbnail_url;
}

public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnail_url = thumbnailUrl;
}

public String getUploader() {
return uploader;
}

public void setUploader(String uploader) {
this.uploader = uploader;
}

public String getUploaderUrl() {
return uploader_url;
}

public void setUploaderUrl(String uploaderUrl) {
this.uploader_url = uploaderUrl;
}

public String getUploaderAvatar() {
return uploader_avatar;
}

public void setUploaderAvatar(String uploaderAvatar) {
this.uploader_avatar = uploaderAvatar;
}

public long getVideoCount() {
return video_count;
}

public void setVideoCount(long videoCount) {
this.video_count = videoCount;
}

public User getOwner() {
return owner;
}

public void setOwner(User owner) {
this.owner = owner;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package me.kavin.piped.utils.resp;

public class BookmarkedStatusResponse {
public boolean bookmarked;

public BookmarkedStatusResponse(boolean bookmarked) {
this.bookmarked = bookmarked;
}
}
1 change: 1 addition & 0 deletions src/main/resources/changelog/db.changelog-master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

<include file="version/0-init.xml" relativeToChangelogFile="true"/>
<include file="version/1-fix-subs.xml" relativeToChangelogFile="true"/>
<include file="version/2-playlist-bookmarks.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Loading