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.*; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.SyndFeedOutput; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import me.kavin.piped.consts.Constants; import me.kavin.piped.ipfs.IPFS; import me.kavin.piped.utils.obj.*; 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; import me.kavin.piped.utils.obj.search.SearchChannel; import me.kavin.piped.utils.obj.search.SearchPlaylist; import me.kavin.piped.utils.resp.*; import okhttp3.FormBody; import okhttp3.Request; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.Session; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.kiosk.KioskExtractor; import org.schabi.newpipe.extractor.kiosk.KioskInfo; import org.schabi.newpipe.extractor.kiosk.KioskList; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Root; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; import static java.nio.charset.StandardCharsets.UTF_8; import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE; import static me.kavin.piped.utils.URLUtils.rewriteURL; import static me.kavin.piped.utils.URLUtils.substringYouTube; import static org.schabi.newpipe.extractor.NewPipe.getPreferredContentCountry; import static org.schabi.newpipe.extractor.NewPipe.getPreferredLocalization; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; public class ResponseHelper { public static byte[] streamsResponse(String videoId) throws Exception { final var futureStream = Multithreading.supplyAsync(() -> { try { return StreamInfo.getInfo("https://www.youtube.com/watch?v=" + videoId); } catch (Exception e) { ExceptionUtils.rethrow(e); } return null; }); final var futureLbryId = Multithreading.supplyAsync(() -> { try { return LbryHelper.getLBRYId(videoId); } catch (Exception e) { ExceptionHandler.handle(e); } return null; }); final var futureLBRY = Multithreading.supplyAsync(() -> { try { String lbryId = futureLbryId.get(2, TimeUnit.SECONDS); return LbryHelper.getLBRYStreamURL(lbryId); } catch (Exception e) { ExceptionHandler.handle(e); } return null; }); final List subtitles = new ObjectArrayList<>(); final List chapters = new ObjectArrayList<>(); final StreamInfo info = futureStream.get(); info.getStreamSegments().forEach(segment -> chapters.add(new ChapterSegment(segment.getTitle(), rewriteURL(segment.getPreviewUrl()), segment.getStartTimeSeconds()))); info.getSubtitles() .forEach(subtitle -> subtitles.add(new Subtitle(rewriteURL(subtitle.getUrl()), subtitle.getFormat().getMimeType(), subtitle.getDisplayLanguageName(), subtitle.getLanguageTag(), subtitle.isAutoGenerated()))); final List videoStreams = new ObjectArrayList<>(); final List audioStreams = new ObjectArrayList<>(); String lbryURL = null; try { lbryURL = futureLBRY.get(3, TimeUnit.SECONDS); } catch (Exception e) { // ignored } if (lbryURL != null) videoStreams.add(new PipedStream(lbryURL, "MP4", "LBRY", "video/mp4", false)); boolean livestream = info.getStreamType() == StreamType.LIVE_STREAM; if (!livestream) { info.getVideoOnlyStreams().forEach(stream -> videoStreams.add(new PipedStream(rewriteURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), true, stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec(), stream.getWidth(), stream.getHeight(), 30))); info.getVideoStreams() .forEach(stream -> videoStreams .add(new PipedStream(rewriteURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getResolution(), stream.getFormat().getMimeType(), false))); info.getAudioStreams() .forEach(stream -> audioStreams.add(new PipedStream(rewriteURL(stream.getUrl()), String.valueOf(stream.getFormat()), stream.getAverageBitrate() + " kbps", stream.getFormat().getMimeType(), false, stream.getBitrate(), stream.getInitStart(), stream.getInitEnd(), stream.getIndexStart(), stream.getIndexEnd(), stream.getCodec()))); } final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> { if (o instanceof StreamInfoItem) relatedStreams.add(collectRelatedStream(o)); }); long time = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : System.currentTimeMillis(); if (info.getUploadDate() != null && System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) updateVideo(info.getId(), info, time); final Streams streams = new Streams(info.getName(), info.getDescription().getContent(), info.getTextualUploadDate(), info.getUploaderName(), substringYouTube(info.getUploaderUrl()), rewriteURL(info.getUploaderAvatarUrl()), rewriteURL(info.getThumbnailUrl()), info.getDuration(), info.getViewCount(), info.getLikeCount(), info.getDislikeCount(), info.getUploaderSubscriberCount(), info.isUploaderVerified(), audioStreams, videoStreams, relatedStreams, subtitles, livestream, rewriteURL(info.getHlsUrl()), rewriteURL(info.getDashMpdUrl()), futureLbryId.get(2, TimeUnit.SECONDS), chapters); return Constants.mapper.writeValueAsBytes(streams); } public static byte[] resolveClipId(String clipId) throws Exception { final byte[] body = JsonWriter.string(prepareDesktopJsonBuilder( getPreferredLocalization(), getPreferredContentCountry()) .value("url", "https://www.youtube.com/clip/" + clipId) .done()) .getBytes(UTF_8); final JsonObject jsonResponse = getJsonPostResponse("navigation/resolve_url", body, getPreferredLocalization()); final String videoId = JsonUtils.getString(jsonResponse, "endpoint.watchEndpoint.videoId"); return Constants.mapper.writeValueAsBytes(new VideoResolvedResponse(videoId)); } public static byte[] channelResponse(String channelPath) throws Exception { final ChannelInfo info = ChannelInfo.getInfo("https://youtube.com/" + channelPath); final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); Multithreading.runAsync(() -> { try (Session s = DatabaseSessionFactory.createSession()) { me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(s, info.getId()); if (channel != null) { if (channel.isVerified() != info.isVerified() || !channel.getUploaderAvatar().equals(info.getAvatarUrl())) { channel.setVerified(info.isVerified()); channel.setUploaderAvatar(info.getAvatarUrl()); if (!s.getTransaction().isActive()) s.getTransaction().begin(); s.update(channel); s.getTransaction().commit(); } for (StreamInfoItem item : info.getRelatedItems()) { long time = item.getUploadDate() != null ? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : System.currentTimeMillis(); if (System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) try { String id = YOUTUBE_SERVICE.getStreamLHFactory().getId(item.getUrl()); updateVideo(id, item, time, true); } catch (Exception e) { ExceptionHandler.handle(e); } } } } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } final Channel channel = new Channel(info.getId(), info.getName(), rewriteURL(info.getAvatarUrl()), rewriteURL(info.getBannerUrl()), info.getDescription(), info.getSubscriberCount(), info.isVerified(), nextpage, relatedStreams); IPFS.publishData(channel); return Constants.mapper.writeValueAsBytes(channel); } public static byte[] channelPageResponse(String channelId, String prevpageStr) throws IOException, ExtractionException { Page prevpage = Constants.mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = ChannelInfo.getMoreItems(YOUTUBE_SERVICE, "https://youtube.com/channel/" + channelId, prevpage); final List relatedStreams = new ObjectArrayList<>(); info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams); return Constants.mapper.writeValueAsBytes(streamspage); } public static byte[] trendingResponse(String region) throws ExtractionException, IOException { if (region == null) return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse()); final List relatedStreams = new ObjectArrayList<>(); KioskList kioskList = YOUTUBE_SERVICE.getKioskList(); kioskList.forceContentCountry(new ContentCountry(region)); KioskExtractor extractor = kioskList.getDefaultKioskExtractor(); extractor.fetchPage(); KioskInfo info = KioskInfo.getInfo(extractor); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); return Constants.mapper.writeValueAsBytes(relatedStreams); } 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") .fetch("channel", 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.uniqueResult(); if (pl == null) return Constants.mapper.writeValueAsBytes(Constants.mapper.createObjectNode() .put("error", "Playlist not found")); 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); final List relatedStreams = new ObjectArrayList<>(); info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } final Playlist playlist = new Playlist(info.getName(), rewriteURL(info.getThumbnailUrl()), rewriteURL(info.getBannerUrl()), nextpage, info.getUploaderName().isEmpty() ? null : info.getUploaderName(), substringYouTube(info.getUploaderUrl()), rewriteURL(info.getUploaderAvatarUrl()), (int) info.getStreamCount(), relatedStreams); return Constants.mapper.writeValueAsBytes(playlist); } public static byte[] playlistPageResponse(String playlistId, String prevpageStr) throws IOException, ExtractionException { Page prevpage = Constants.mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE, "https://www.youtube.com/playlist?list=" + playlistId, prevpage); final List relatedStreams = new ObjectArrayList<>(); info.getItems().forEach(o -> relatedStreams.add(collectRelatedStream(o))); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams); return Constants.mapper.writeValueAsBytes(streamspage); } public static byte[] playlistRSSResponse(String playlistId) throws IOException, ExtractionException, FeedException { final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId); final List entries = new ObjectArrayList<>(); SyndFeed feed = new SyndFeedImpl(); feed.setFeedType("rss_2.0"); feed.setTitle(info.getName()); feed.setAuthor(info.getUploaderName()); feed.setDescription(String.format("%s - Piped", info.getName())); feed.setLink(Constants.FRONTEND_URL + substringYouTube(info.getUrl())); feed.setPublishedDate(new Date()); info.getRelatedItems().forEach(item -> { SyndEntry entry = new SyndEntryImpl(); entry.setAuthor(item.getUploaderName()); entry.setLink(item.getUrl()); entry.setUri(item.getUrl()); entry.setTitle(item.getName()); entries.add(entry); }); feed.setEntries(entries); return new SyndFeedOutput().outputString(feed).getBytes(UTF_8); } public static byte[] suggestionsResponse(String query) throws IOException, ExtractionException { return Constants.mapper.writeValueAsBytes(YOUTUBE_SERVICE.getSuggestionExtractor().suggestionList(query)); } public static byte[] opensearchSuggestionsResponse(String query) throws IOException, ExtractionException { return Constants.mapper.writeValueAsBytes(Arrays.asList( query, YOUTUBE_SERVICE.getSuggestionExtractor().suggestionList(query) )); } public static byte[] searchResponse(String q, String filter) throws IOException, ExtractionException { final SearchInfo info = SearchInfo.getInfo(YOUTUBE_SERVICE, YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null)); ObjectArrayList items = new ObjectArrayList<>(); info.getRelatedItems().forEach(item -> { switch (item.getInfoType()) { case STREAM: items.add(collectRelatedStream(item)); break; case CHANNEL: ChannelInfoItem channel = (ChannelInfoItem) item; items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(), channel.getStreamCount(), channel.isVerified())); break; case PLAYLIST: PlaylistInfoItem playlist = (PlaylistInfoItem) item; items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount())); break; default: break; } }); Page nextpage = info.getNextPage(); return Constants.mapper.writeValueAsBytes(new SearchResults(items, Constants.mapper.writeValueAsString(nextpage), info.getSearchSuggestion(), info.isCorrectedSearch())); } public static byte[] searchPageResponse(String q, String filter, String prevpageStr) throws IOException, ExtractionException { Page prevpage = Constants.mapper.readValue(prevpageStr, Page.class); InfoItemsPage pages = SearchInfo.getMoreItems(YOUTUBE_SERVICE, YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null), prevpage); ObjectArrayList items = new ObjectArrayList<>(); pages.getItems().forEach(item -> { switch (item.getInfoType()) { case STREAM: items.add(collectRelatedStream(item)); break; case CHANNEL: ChannelInfoItem channel = (ChannelInfoItem) item; items.add(new SearchChannel(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), channel.getDescription(), channel.getSubscriberCount(), channel.getStreamCount(), channel.isVerified())); break; case PLAYLIST: PlaylistInfoItem playlist = (PlaylistInfoItem) item; items.add(new SearchPlaylist(item.getName(), rewriteURL(item.getThumbnailUrl()), substringYouTube(item.getUrl()), playlist.getUploaderName(), playlist.getStreamCount())); break; default: break; } }); Page nextpage = pages.getNextPage(); return Constants.mapper .writeValueAsBytes(new SearchResults(items, Constants.mapper.writeValueAsString(nextpage))); } public static byte[] commentsResponse(String videoId) throws Exception { CommentsInfo info = CommentsInfo.getInfo("https://www.youtube.com/watch?v=" + videoId); List comments = new ObjectArrayList<>(); info.getRelatedItems().forEach(comment -> { try { String repliespage = null; if (comment.getReplies() != null) repliespage = Constants.mapper.writeValueAsString(comment.getReplies()); comments.add(new Comment(comment.getUploaderName(), rewriteURL(comment.getUploaderAvatarUrl()), comment.getCommentId(), comment.getCommentText(), comment.getTextualUploadDate(), substringYouTube(comment.getUploaderUrl()), repliespage, comment.getLikeCount(), comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified())); } catch (JsonProcessingException e) { ExceptionHandler.handle(e); } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } CommentsPage commentsItem = new CommentsPage(comments, nextpage, info.isCommentsDisabled()); return Constants.mapper.writeValueAsBytes(commentsItem); } public static byte[] commentsPageResponse(String videoId, String prevpageStr) throws Exception { Page prevpage = Constants.mapper.readValue(prevpageStr, Page.class); InfoItemsPage info = CommentsInfo.getMoreItems(YOUTUBE_SERVICE, "https://www.youtube.com/watch?v=" + videoId, prevpage); List comments = new ObjectArrayList<>(); info.getItems().forEach(comment -> { try { String repliespage = null; if (comment.getReplies() != null) repliespage = Constants.mapper.writeValueAsString(comment.getReplies()); comments.add(new Comment(comment.getUploaderName(), rewriteURL(comment.getUploaderAvatarUrl()), comment.getCommentId(), comment.getCommentText(), comment.getTextualUploadDate(), substringYouTube(comment.getUploaderUrl()), repliespage, comment.getLikeCount(), comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified())); } catch (JsonProcessingException e) { ExceptionHandler.handle(e); } }); String nextpage = null; if (info.hasNextPage()) { Page page = info.getNextPage(); nextpage = Constants.mapper.writeValueAsString(page); } CommentsPage commentsItem = new CommentsPage(comments, nextpage, false); return Constants.mapper.writeValueAsBytes(commentsItem); } private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(); public static byte[] registerResponse(String user, String pass) throws IOException { if (Constants.DISABLE_REGISTRATION) return Constants.mapper.writeValueAsBytes(new DisabledRegistrationResponse()); if (StringUtils.isBlank(user) || StringUtils.isBlank(pass)) return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse()); user = user.toLowerCase(); try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); Root root = cr.from(User.class); cr.select(root).where(cb.equal(root.get("username"), user)); boolean registered = s.createQuery(cr).uniqueResult() != null; if (registered) { return Constants.mapper.writeValueAsBytes(new AlreadyRegisteredResponse()); } if (Constants.COMPROMISED_PASSWORD_CHECK) { String sha1Hash = DigestUtils.sha1Hex(pass).toUpperCase(); String prefix = sha1Hash.substring(0, 5); String suffix = sha1Hash.substring(5); String[] entries = RequestUtils .sendGet("https://api.pwnedpasswords.com/range/" + prefix, "github.com/TeamPiped/Piped-Backend") .split("\n"); for (String entry : entries) if (StringUtils.substringBefore(entry, ":").equals(suffix)) return Constants.mapper.writeValueAsBytes(new CompromisedPasswordResponse()); } User newuser = new User(user, argon2PasswordEncoder.encode(pass), Collections.emptyList()); s.save(newuser); s.getTransaction().begin(); s.getTransaction().commit(); return Constants.mapper.writeValueAsBytes(new LoginResponse(newuser.getSessionId())); } } private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder(); public static byte[] loginResponse(String user, String pass) throws IOException { if (user == null || pass == null) return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse()); user = user.toLowerCase(); try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(User.class); Root root = cr.from(User.class); cr.select(root).where(root.get("username").in(user)); User dbuser = s.createQuery(cr).uniqueResult(); if (dbuser != null) { String hash = dbuser.getPassword(); if (hash.startsWith("$argon2")) { if (argon2PasswordEncoder.matches(pass, hash)) { return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId())); } } else if (bcryptPasswordEncoder.matches(pass, hash)) { return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId())); } } return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse()); } } public static byte[] subscribeResponse(String session, String channelId) throws IOException { try (Session s = DatabaseSessionFactory.createSession()) { User user = DatabaseHelper.getUserFromSessionWithSubscribed(session); if (user != null) { if (!user.getSubscribed().contains(channelId)) { s.getTransaction().begin(); s.createNativeQuery("insert into users_subscribed (subscriber, channel) values (?,?)") .setParameter(1, user.getId()).setParameter(2, channelId).executeUpdate(); s.getTransaction().commit(); Multithreading.runAsync(() -> { try (Session sess = DatabaseSessionFactory.createSession()) { var channel = DatabaseHelper.getChannelFromId(sess, channelId); if (channel == null) { ChannelInfo info = null; try { info = ChannelInfo.getInfo("https://youtube.com/channel/" + channelId); } catch (IOException | ExtractionException e) { ExceptionUtils.rethrow(e); } channel = new me.kavin.piped.utils.obj.db.Channel(channelId, info.getName(), info.getAvatarUrl(), info.isVerified()); sess.save(channel); sess.beginTransaction().commit(); Multithreading.runAsync(() -> { try (Session sessSub = DatabaseSessionFactory.createSession()) { subscribePubSub(channelId, sessSub); } catch (Exception e) { ExceptionHandler.handle(e); } }); for (StreamInfoItem item : info.getRelatedItems()) { long time = item.getUploadDate() != null ? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : System.currentTimeMillis(); if ((System.currentTimeMillis() - time) < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) handleNewVideo(item.getUrl(), time, channel, sess); } } } }); } return Constants.mapper.writeValueAsBytes(new AcceptedResponse()); } return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } } public static byte[] unsubscribeResponse(String session, String channelId) throws IOException { User user = DatabaseHelper.getUserFromSession(session); if (user != null) { try (Session s = DatabaseSessionFactory.createSession()) { s.getTransaction().begin(); s.createNativeQuery("delete from users_subscribed where subscriber = :id and channel = :channel") .setParameter("id", user.getId()).setParameter("channel", channelId).executeUpdate(); s.getTransaction().commit(); return Constants.mapper.writeValueAsBytes(new AcceptedResponse()); } } return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } public static byte[] isSubscribedResponse(String session, String channelId) throws IOException { try (Session s = DatabaseSessionFactory.createSession()) { var cb = s.getCriteriaBuilder(); var query = cb.createQuery(Long.class); var root = query.from(User.class); query.select(cb.count(root)) .where(cb.and( cb.equal(root.get("sessionId"), session), cb.isMember(channelId, root.get("subscribed_ids")) )); var subscribed = s.createQuery(query).getSingleResult() > 0; return Constants.mapper.writeValueAsBytes(new SubscribeStatusResponse(subscribed)); } } public static byte[] feedResponse(String session) throws IOException { User user = DatabaseHelper.getUserFromSession(session); if (user != null) { try (Session s = DatabaseSessionFactory.createSession()) { CriteriaBuilder cb = s.getCriteriaBuilder(); // Get all videos from subscribed channels, with channel info CriteriaQuery