From 056d61f2d24c17fa309d24a59464e187a1de21a1 Mon Sep 17 00:00:00 2001 From: FireMaskterK <20838718+FireMasterK@users.noreply.github.com> Date: Fri, 3 Dec 2021 23:39:46 +0000 Subject: [PATCH 01/12] Add playlist object. --- .../piped/utils/DatabaseSessionFactory.java | 7 +- .../me/kavin/piped/utils/obj/db/Playlist.java | 95 +++++++++++++++++++ .../piped/utils/obj/db/PlaylistVideo.java | 84 ++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/Playlist.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/PlaylistVideo.java diff --git a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java index a1bd10f..63fefd4 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java @@ -2,6 +2,8 @@ package me.kavin.piped.utils; import me.kavin.piped.consts.Constants; import me.kavin.piped.utils.obj.db.Channel; +import me.kavin.piped.utils.obj.db.Playlist; +import me.kavin.piped.utils.obj.db.PlaylistVideo; import me.kavin.piped.utils.obj.db.PubSub; import me.kavin.piped.utils.obj.db.User; import me.kavin.piped.utils.obj.db.Video; @@ -21,8 +23,9 @@ public class DatabaseSessionFactory { configuration.setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false"); configuration.configure(); - sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Video.class) - .addAnnotatedClass(Channel.class).addAnnotatedClass(PubSub.class).buildSessionFactory(); + sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class) + .addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class) + .addAnnotatedClass(PlaylistVideo.class).buildSessionFactory(); } public static final Session createSession() { diff --git a/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java new file mode 100644 index 0000000..ba65b68 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java @@ -0,0 +1,95 @@ +package me.kavin.piped.utils.obj.db; + +import java.util.List; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name = "playlists", indexes = { @Index(columnList = "playlist_id", name = "playlists_playlist_id_idx") }) +public class Playlist { + + public Playlist() { + } + + public Playlist(String name, List videos, String thumbnail) { + this.name = name; + this.videos = videos; + this.thumbnail = thumbnail; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "playlist_id") + private UUID playlist_id; + + @Column(name = "name", length = 100) + private String name; + + @Column(name = "thumbnail", length = 255) + private String thumbnail; + + @Column(name = "owner") + private long owner; + + @ElementCollection(fetch = FetchType.LAZY) + @OneToMany(targetEntity = PlaylistVideo.class) + @Column(name = "videos") + private List videos; + + public long getId() { + return id; + } + + public UUID getPlaylistId() { + return playlist_id; + } + + public void setPlaylistId(UUID playlist_id) { + this.playlist_id = playlist_id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getVideos() { + return videos; + } + + public void setVideos(List videos) { + this.videos = videos; + } + + public String getThumbnail() { + return thumbnail; + } + + public void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + + public long getOwner() { + return owner; + } + + public void setOwner(long owner) { + this.owner = owner; + } +} diff --git a/src/main/java/me/kavin/piped/utils/obj/db/PlaylistVideo.java b/src/main/java/me/kavin/piped/utils/obj/db/PlaylistVideo.java new file mode 100644 index 0000000..5f170e6 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/obj/db/PlaylistVideo.java @@ -0,0 +1,84 @@ +package me.kavin.piped.utils.obj.db; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "playlist_videos", indexes = { @Index(columnList = "id", name = "playlist_videos_id_idx"), + @Index(columnList = "uploader_id", name = "playlist_videos_uploader_id_idx") }) +public class PlaylistVideo { + + public PlaylistVideo() { + } + + public PlaylistVideo(String id, String title, long duration, String thumbnail, Channel channel) { + this.id = id; + this.title = title; + this.duration = duration; + this.thumbnail = thumbnail; + this.channel = channel; + } + + @Id + @Column(name = "id", unique = true, length = 16) + private String id; + + @Column(name = "title", length = 120) + private String title; + + @Column(name = "duration") + private long duration; + + @Column(name = "thumbnail", length = 150) + private String thumbnail; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "uploader_id") + private Channel channel; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getThumbnail() { + return thumbnail; + } + + public void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } +} From b049848e791981cea3c1c998e999a53d6656e27f Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Wed, 6 Apr 2022 03:35:42 +0100 Subject: [PATCH 02/12] Add playlist creation route. --- .../java/me/kavin/piped/ServerLauncher.java | 13 +++ .../me/kavin/piped/utils/ResponseHelper.java | 91 ++++++++++++++++++- .../me/kavin/piped/utils/obj/db/Playlist.java | 47 +++++----- 3 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/main/java/me/kavin/piped/ServerLauncher.java b/src/main/java/me/kavin/piped/ServerLauncher.java index 120bc44..a1750ef 100644 --- a/src/main/java/me/kavin/piped/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/ServerLauncher.java @@ -278,6 +278,19 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher { } catch (Exception e) { return getErrorResponse(e, request.getPath()); } + })).map(POST, "/user/playlists/create", AsyncServlet.ofBlocking(executor, request -> { + try { + var name = Constants.mapper.readTree(request.loadBody().getResult().asArray()).get("name").asText(); + return getJsonResponse(ResponseHelper.createPlaylist(request.getHeader(AUTHORIZATION), name), "private"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } + })).map(GET, "/user/playlists", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.playlistsResponse(request.getHeader(AUTHORIZATION)), "private"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } })).map(GET, "/registered/badge", AsyncServlet.ofBlocking(executor, request -> { try { return HttpResponse.ofCode(302).withHeader(LOCATION, ResponseHelper.registeredBadgeRedirect()) diff --git a/src/main/java/me/kavin/piped/utils/ResponseHelper.java b/src/main/java/me/kavin/piped/utils/ResponseHelper.java index 16306f8..6a8dd24 100644 --- a/src/main/java/me/kavin/piped/utils/ResponseHelper.java +++ b/src/main/java/me/kavin/piped/utils/ResponseHelper.java @@ -1,6 +1,7 @@ package me.kavin.piped.utils; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonWriter; import com.rometools.rome.feed.synd.*; @@ -278,7 +279,46 @@ public class ResponseHelper { return Constants.mapper.writeValueAsBytes(relatedStreams); } - public static byte[] playlistResponse(String playlistId) + public static byte[] playlistResponse(String playlistId) throws ExtractionException, IOException { + + if (StringUtils.isBlank(playlistId)) + return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse()); + + if (playlistId.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")) + return playlistPipedResponse(playlistId); + + return playlistYouTubeResponse(playlistId); + } + + private static byte[] playlistPipedResponse(String playlistId) throws IOException { + try (Session s = DatabaseSessionFactory.createSession()) { + var cb = s.getCriteriaBuilder(); + var cq = cb.createQuery(me.kavin.piped.utils.obj.db.Playlist.class); + var root = cq.from(me.kavin.piped.utils.obj.db.Playlist.class); + root.fetch("videos", JoinType.LEFT); + root.fetch("owner", JoinType.LEFT); + cq.select(root); + cq.where(cb.equal(root.get("playlist_id"), UUID.fromString(playlistId))); + var query = s.createQuery(cq); + var pl = query.getSingleResult(); + + final List relatedStreams = new ObjectArrayList<>(); + + for (var video : pl.getVideos()) { + var channel = video.getChannel(); + relatedStreams.add(new StreamItem("/watch?v=" + video.getId(), video.getTitle(), rewriteURL(video.getThumbnail()), channel.getUploader(), + "/channel/" + channel.getUploaderId(), rewriteURL(channel.getUploaderAvatar()), null, null, + video.getDuration(), -1, -1, channel.isVerified())); + } + + final Playlist playlist = new Playlist(pl.getName(), rewriteURL(pl.getThumbnail()), null, null, pl.getOwner().getUsername(), + null, null, -1, relatedStreams); + + return Constants.mapper.writeValueAsBytes(playlist); + } + } + + private static byte[] playlistYouTubeResponse(String playlistId) throws IOException, ExtractionException { final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId); @@ -910,6 +950,55 @@ public class ResponseHelper { } + public static byte[] createPlaylist(String session, String name) throws IOException { + + if (StringUtils.isBlank(name)) + return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse()); + + User user = DatabaseHelper.getUserFromSession(session); + + if (user == null) + return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse()); + + try (Session s = DatabaseSessionFactory.createSession()) { + var playlist = new me.kavin.piped.utils.obj.db.Playlist(name, user, "https://i.ytimg.com/"); + s.save(playlist); + s.getTransaction().begin(); + s.getTransaction().commit(); + } + + return Constants.mapper.writeValueAsBytes(new AcceptedResponse()); + } + + public static byte[] playlistsResponse(String session) throws IOException { + + User user = DatabaseHelper.getUserFromSession(session); + + if (user == null) + return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse()); + + try (Session s = DatabaseSessionFactory.createSession()) { + var cb = s.getCriteriaBuilder(); + var query = cb.createQuery(me.kavin.piped.utils.obj.db.Playlist.class); + var root = query.from(me.kavin.piped.utils.obj.db.Playlist.class); + query.select(root); + query.where(cb.equal(root.get("owner"), user)); + + var playlists = new ObjectArrayList<>(); + + for (var playlist : s.createQuery(query).list()) { + ObjectNode node = Constants.mapper.createObjectNode(); + node.put("id", String.valueOf(playlist.getPlaylistId())); + node.put("name", playlist.getName()); + node.put("shortDescription", playlist.getShortDescription()); + node.put("thumbnail", rewriteURL(playlist.getThumbnail())); + playlists.add(node); + } + + return Constants.mapper.writeValueAsBytes(playlists); + } + } + public static String registeredBadgeRedirect() { try (Session s = DatabaseSessionFactory.createSession()) { long registered = (Long) s.createQuery("select count(*) from User").uniqueResult(); diff --git a/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java index ba65b68..bbcd4ca 100644 --- a/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java +++ b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java @@ -1,48 +1,45 @@ package me.kavin.piped.utils.obj.db; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import javax.persistence.*; import java.util.List; import java.util.UUID; -import javax.persistence.Column; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.OneToMany; -import javax.persistence.Table; - @Entity -@Table(name = "playlists", indexes = { @Index(columnList = "playlist_id", name = "playlists_playlist_id_idx") }) +@Table(name = "playlists", indexes = {@Index(columnList = "playlist_id", name = "playlists_playlist_id_idx")}) public class Playlist { public Playlist() { } - public Playlist(String name, List videos, String thumbnail) { + public Playlist(String name, User owner, String thumbnail) { this.name = name; - this.videos = videos; + this.owner = owner; + this.videos = new ObjectArrayList<>(); this.thumbnail = thumbnail; + this.playlist_id = UUID.randomUUID(); } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "playlist_id") + @Column(name = "playlist_id", unique = true, nullable = false) + @GeneratedValue(generator = "UUID", strategy = GenerationType.IDENTITY) private UUID playlist_id; @Column(name = "name", length = 100) private String name; - @Column(name = "thumbnail", length = 255) + @Column(name = "short_description", length = 100) + private String short_description; + + @Column(name = "thumbnail", length = 300) private String thumbnail; - @Column(name = "owner") - private long owner; + @Column(name = "owner", nullable = false) + private User owner; @ElementCollection(fetch = FetchType.LAZY) @OneToMany(targetEntity = PlaylistVideo.class) @@ -77,6 +74,14 @@ public class Playlist { this.videos = videos; } + public String getShortDescription() { + return short_description; + } + + public void setShortDescription(String short_description) { + this.short_description = short_description; + } + public String getThumbnail() { return thumbnail; } @@ -85,11 +90,11 @@ public class Playlist { this.thumbnail = thumbnail; } - public long getOwner() { + public User getOwner() { return owner; } - public void setOwner(long owner) { + public void setOwner(User owner) { this.owner = owner; } } From 797a621b5129265d0d5e0b4c3401084aa9f0b0b1 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Wed, 6 Apr 2022 04:14:16 +0100 Subject: [PATCH 03/12] Add tests for playlist. --- .../java/me/kavin/piped/utils/ResponseHelper.java | 7 +++++-- .../java/me/kavin/piped/utils/obj/db/Playlist.java | 3 ++- testing/api-test.sh | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/kavin/piped/utils/ResponseHelper.java b/src/main/java/me/kavin/piped/utils/ResponseHelper.java index 6a8dd24..55c4395 100644 --- a/src/main/java/me/kavin/piped/utils/ResponseHelper.java +++ b/src/main/java/me/kavin/piped/utils/ResponseHelper.java @@ -965,9 +965,12 @@ public class ResponseHelper { s.save(playlist); s.getTransaction().begin(); s.getTransaction().commit(); - } - return Constants.mapper.writeValueAsBytes(new AcceptedResponse()); + ObjectNode response = Constants.mapper.createObjectNode(); + response.put("playlistId", String.valueOf(playlist.getPlaylistId())); + + return Constants.mapper.writeValueAsBytes(response); + } } public static byte[] playlistsResponse(String session) throws IOException { diff --git a/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java index bbcd4ca..3948c19 100644 --- a/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java +++ b/src/main/java/me/kavin/piped/utils/obj/db/Playlist.java @@ -38,7 +38,8 @@ public class Playlist { @Column(name = "thumbnail", length = 300) private String thumbnail; - @Column(name = "owner", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner") private User owner; @ElementCollection(fetch = FetchType.LAZY) diff --git a/testing/api-test.sh b/testing/api-test.sh index 045bf11..155c3c8 100755 --- a/testing/api-test.sh +++ b/testing/api-test.sh @@ -84,3 +84,17 @@ sleep 2 # Check Feed curl ${CURLOPTS[@]} $HOST/feed -G --data-urlencode "authToken=$AUTH_TOKEN" || exit 1 + +PLAYLIST_NAME=$(openssl rand -hex 6) + +# Create a Playlist +curl ${CURLOPTS[@]} $HOST/user/playlists/create -X POST -H "Content-Type: application/json" -H "Authorization: $AUTH_TOKEN" -d $(jq -n --compact-output --arg name "$PLAYLIST_NAME" '{"name": $name}') || exit 1 + +# See created playlists +curl ${CURLOPTS[@]} $HOST/user/playlists -H "Authorization: $AUTH_TOKEN" || exit 1 + +# Get Playlist ID +PLAYLIST_ID=$(curl -s -o - -f $HOST/user/playlists -H "Authorization: $AUTH_TOKEN" | jq -r ".[0].id") || exit 1 + +# Playlist Test +curl ${CURLOPTS[@]} $HOST/playlists/$PLAYLIST_ID || exit 1 From 00e32dc992441550354d634bf6f8191d3370f919 Mon Sep 17 00:00:00 2001 From: Kavin <20838718+FireMasterK@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:07:50 +0100 Subject: [PATCH 04/12] Add route to add video to playlist. --- .../java/me/kavin/piped/ServerLauncher.java | 9 +++ .../me/kavin/piped/utils/DatabaseHelper.java | 24 ++++--- .../me/kavin/piped/utils/ResponseHelper.java | 72 ++++++++++++++++++- .../piped/utils/obj/db/PlaylistVideo.java | 17 ++--- testing/api-test.sh | 3 + 5 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/main/java/me/kavin/piped/ServerLauncher.java b/src/main/java/me/kavin/piped/ServerLauncher.java index a1750ef..4572eda 100644 --- a/src/main/java/me/kavin/piped/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/ServerLauncher.java @@ -291,6 +291,15 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher { } catch (Exception e) { return getErrorResponse(e, request.getPath()); } + })).map(POST, "/user/playlists/add", AsyncServlet.ofBlocking(executor, request -> { + try { + var json = Constants.mapper.readTree(request.loadBody().getResult().asArray()); + var playlistId = json.get("playlistId").asText(); + var videoId = json.get("videoId").asText(); + return getJsonResponse(ResponseHelper.addToPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, videoId), "private"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } })).map(GET, "/registered/badge", AsyncServlet.ofBlocking(executor, request -> { try { return HttpResponse.ofCode(302).withHeader(LOCATION, ResponseHelper.registeredBadgeRedirect()) diff --git a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java index ce1fbae..75aed1b 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java @@ -1,9 +1,6 @@ package me.kavin.piped.utils; -import me.kavin.piped.utils.obj.db.Channel; -import me.kavin.piped.utils.obj.db.PubSub; -import me.kavin.piped.utils.obj.db.User; -import me.kavin.piped.utils.obj.db.Video; +import me.kavin.piped.utils.obj.db.*; import org.hibernate.Session; import javax.persistence.criteria.CriteriaBuilder; @@ -13,7 +10,7 @@ import javax.persistence.criteria.Root; public class DatabaseHelper { - public static final User getUserFromSession(String session) { + public static User getUserFromSession(String session) { try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); @@ -24,7 +21,7 @@ public class DatabaseHelper { } } - public static final User getUserFromSessionWithSubscribed(String session) { + public static User getUserFromSessionWithSubscribed(String session) { try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); @@ -36,7 +33,7 @@ public class DatabaseHelper { } } - public static final Channel getChannelFromId(Session s, String id) { + public static Channel getChannelFromId(Session s, String id) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(Channel.class); Root root = cr.from(Channel.class); @@ -45,7 +42,7 @@ public class DatabaseHelper { return s.createQuery(cr).uniqueResult(); } - public static final Video getVideoFromId(Session s, String id) { + public static Video getVideoFromId(Session s, String id) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery