mirror of
https://github.com/TeamPiped/Piped-Backend
synced 2025-09-05 21:11:05 +02:00

If the sessionId is invalid, false should be returned for subscribed, we make it the client's responsibility to ensure the sessionId is valid.
1122 lines
46 KiB
Java
1122 lines
46 KiB
Java
package me.kavin.piped.utils;
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
import com.github.benmanes.caffeine.cache.LoadingCache;
|
|
import com.github.benmanes.caffeine.cache.Scheduler;
|
|
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.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.CompletableFuture;
|
|
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 final LoadingCache<String, CommentsInfo> commentsCache = Caffeine.newBuilder()
|
|
.expireAfterWrite(1, TimeUnit.HOURS)
|
|
.scheduler(Scheduler.systemScheduler())
|
|
.maximumSize(1000).build(key -> CommentsInfo.getInfo("https://www.youtube.com/watch?v=" + key));
|
|
|
|
public static byte[] streamsResponse(String videoId) throws Exception {
|
|
|
|
CompletableFuture<StreamInfo> futureStream = CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
return StreamInfo.getInfo("https://www.youtube.com/watch?v=" + videoId);
|
|
} catch (Exception e) {
|
|
ExceptionUtils.rethrow(e);
|
|
}
|
|
return null;
|
|
}, Multithreading.getCachedExecutor());
|
|
|
|
CompletableFuture<String> futureLbryId = CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
return LbryHelper.getLBRYId(videoId);
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
return null;
|
|
}, Multithreading.getCachedExecutor());
|
|
|
|
CompletableFuture<String> futureLBRY = CompletableFuture.supplyAsync(() -> {
|
|
try {
|
|
String lbryId = futureLbryId.completeOnTimeout(null, 2, TimeUnit.SECONDS).get();
|
|
|
|
return LbryHelper.getLBRYStreamURL(lbryId);
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
return null;
|
|
}, Multithreading.getCachedExecutor());
|
|
|
|
final List<Subtitle> subtitles = new ObjectArrayList<>();
|
|
final List<ChapterSegment> 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<PipedStream> videoStreams = new ObjectArrayList<>();
|
|
final List<PipedStream> audioStreams = new ObjectArrayList<>();
|
|
|
|
String lbryURL = null;
|
|
|
|
try {
|
|
lbryURL = futureLBRY.completeOnTimeout(null, 3, TimeUnit.SECONDS).get();
|
|
} 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<StreamItem> relatedStreams = new ObjectArrayList<>();
|
|
|
|
info.getRelatedItems().forEach(o -> 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.isUploaderVerified(),
|
|
audioStreams, videoStreams, relatedStreams, subtitles, livestream, rewriteURL(info.getHlsUrl()),
|
|
rewriteURL(info.getDashMpdUrl()), futureLbryId.get(), 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<StreamItem> relatedStreams = new ObjectArrayList<>();
|
|
|
|
info.getRelatedItems().forEach(o -> relatedStreams.add(collectRelatedStream(o)));
|
|
|
|
Multithreading.runAsync(() -> {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
s.close();
|
|
});
|
|
|
|
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<StreamInfoItem> info = ChannelInfo.getMoreItems(YOUTUBE_SERVICE,
|
|
"https://youtube.com/channel/" + channelId, prevpage);
|
|
|
|
final List<StreamItem> 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<StreamItem> 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 IOException, ExtractionException {
|
|
|
|
final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId);
|
|
|
|
final List<StreamItem> 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<StreamInfoItem> info = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE,
|
|
"https://www.youtube.com/playlist?list=" + playlistId, prevpage);
|
|
|
|
final List<StreamItem> 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<SyndEntry> 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<Object> 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<InfoItem> pages = SearchInfo.getMoreItems(YOUTUBE_SERVICE,
|
|
YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null), prevpage);
|
|
|
|
ObjectArrayList<Object> 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 = commentsCache.get(videoId);
|
|
|
|
List<Comment> 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);
|
|
|
|
CommentsInfo init = commentsCache.get(videoId);
|
|
|
|
InfoItemsPage<CommentsInfoItem> info = CommentsInfo.getMoreItems(init, prevpage);
|
|
|
|
List<Comment> 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, init.isCommentsDisabled());
|
|
|
|
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 (user == null || pass == null)
|
|
return Constants.mapper.writeValueAsBytes(new InvalidRequestResponse());
|
|
|
|
user = user.toLowerCase();
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
|
CriteriaQuery<User> cr = cb.createQuery(User.class);
|
|
Root<User> root = cr.from(User.class);
|
|
cr.select(root).where(root.get("username").in(user));
|
|
boolean registered = s.createQuery(cr).uniqueResult() != null;
|
|
|
|
if (registered) {
|
|
s.close();
|
|
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();
|
|
|
|
s.close();
|
|
|
|
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();
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
|
CriteriaQuery<User> cr = cb.createQuery(User.class);
|
|
Root<User> 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)) {
|
|
s.close();
|
|
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
|
}
|
|
} else if (bcryptPasswordEncoder.matches(pass, hash)) {
|
|
s.close();
|
|
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
|
}
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
|
|
|
}
|
|
|
|
public static byte[] subscribeResponse(String session, String channelId)
|
|
throws IOException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, 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();
|
|
s.close();
|
|
|
|
Multithreading.runAsync(() -> {
|
|
Session sess = DatabaseSessionFactory.createSession();
|
|
me.kavin.piped.utils.obj.db.Channel 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);
|
|
sessSub.close();
|
|
} 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);
|
|
}
|
|
}
|
|
|
|
sess.close();
|
|
});
|
|
}
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static byte[] unsubscribeResponse(String session, String channelId)
|
|
throws IOException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSession(s, session);
|
|
|
|
if (user != null) {
|
|
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();
|
|
s.close();
|
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static byte[] isSubscribedResponse(String session, String channelId)
|
|
throws IOException {
|
|
|
|
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;
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new SubscribeStatusResponse(subscribed));
|
|
}
|
|
|
|
public static byte[] feedResponse(String session)
|
|
throws IOException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSession(s, session);
|
|
|
|
if (user != null) {
|
|
|
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
|
|
|
// Get all videos from subscribed channels, with channel info
|
|
CriteriaQuery<Video> criteria = cb.createQuery(Video.class);
|
|
criteria.distinct(true);
|
|
var root = criteria.from(Video.class);
|
|
var userRoot = criteria.from(User.class);
|
|
root.fetch("channel", JoinType.INNER);
|
|
|
|
criteria.select(root)
|
|
.where(cb.and(
|
|
cb.isMember(root.get("channel"), userRoot.<Collection<String>>get("subscribed_ids")),
|
|
cb.equal(userRoot.get("id"), user.getId())
|
|
))
|
|
.orderBy(cb.desc(root.get("uploaded")));
|
|
|
|
List<StreamItem> feedItems = new ObjectArrayList<>();
|
|
|
|
for (Video video : s.createQuery(criteria).list()) {
|
|
var channel = video.getChannel();
|
|
|
|
feedItems.add(new StreamItem("/watch?v=" + video.getId(), video.getTitle(),
|
|
rewriteURL(video.getThumbnail()), channel.getUploader(), "/channel/" + channel.getUploaderId(),
|
|
rewriteURL(channel.getUploaderAvatar()), null, null, video.getDuration(), video.getViews(),
|
|
video.getUploaded(), channel.isVerified()));
|
|
}
|
|
|
|
feedItems.sort(Comparator.<StreamItem>comparingLong(o -> o.uploaded).reversed());
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(feedItems);
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static byte[] feedResponseRSS(String session)
|
|
throws IOException, FeedException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSession(s, session);
|
|
|
|
if (user != null) {
|
|
|
|
SyndFeed feed = new SyndFeedImpl();
|
|
feed.setFeedType("atom_1.0");
|
|
feed.setTitle("Piped - Feed");
|
|
feed.setDescription(String.format("Piped's RSS subscription feed for %s.", user.getUsername()));
|
|
feed.setUri(Constants.FRONTEND_URL + "/feed");
|
|
feed.setPublishedDate(new Date());
|
|
|
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
|
|
|
// Get all videos from subscribed channels, with channel info
|
|
CriteriaQuery<Video> criteria = cb.createQuery(Video.class);
|
|
criteria.distinct(true);
|
|
var root = criteria.from(Video.class);
|
|
var userRoot = criteria.from(User.class);
|
|
root.fetch("channel", JoinType.INNER);
|
|
|
|
criteria.select(root)
|
|
.where(cb.and(
|
|
cb.isMember(root.get("channel"), userRoot.<Collection<String>>get("subscribed_ids")),
|
|
cb.equal(userRoot.get("id"), user.getId())
|
|
))
|
|
.orderBy(cb.desc(root.get("uploaded")));
|
|
|
|
List<Video> videos = s.createQuery(criteria).list();
|
|
|
|
final List<SyndEntry> entries = new ObjectArrayList<>();
|
|
|
|
for (Video video : videos) {
|
|
var channel = video.getChannel();
|
|
SyndEntry entry = new SyndEntryImpl();
|
|
|
|
SyndPerson person = new SyndPersonImpl();
|
|
person.setName(channel.getUploader());
|
|
person.setUri(Constants.FRONTEND_URL + "/channel/" + channel.getUploaderId());
|
|
|
|
entry.setAuthors(Collections.singletonList(person));
|
|
|
|
entry.setLink(Constants.FRONTEND_URL + "/watch?v=" + video.getId());
|
|
entry.setUri(Constants.FRONTEND_URL + "/watch?v=" + video.getId());
|
|
entry.setTitle(video.getTitle());
|
|
entry.setPublishedDate(new Date(video.getUploaded()));
|
|
entries.add(entry);
|
|
}
|
|
|
|
feed.setEntries(entries);
|
|
|
|
|
|
s.close();
|
|
|
|
return new SyndFeedOutput().outputString(feed).getBytes(UTF_8);
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static byte[] importResponse(String session, String[] channelIds, boolean override)
|
|
throws IOException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, session);
|
|
|
|
if (user != null) {
|
|
|
|
Multithreading.runAsync(() -> {
|
|
if (override)
|
|
user.setSubscribed(Arrays.asList(channelIds));
|
|
else
|
|
for (String channelId : channelIds)
|
|
if (!user.getSubscribed().contains(channelId))
|
|
user.getSubscribed().add(channelId);
|
|
|
|
if (channelIds.length > 0) {
|
|
s.update(user);
|
|
s.beginTransaction().commit();
|
|
}
|
|
|
|
s.close();
|
|
});
|
|
|
|
for (String channelId : channelIds) {
|
|
|
|
Multithreading.runAsyncLimited(() -> {
|
|
try {
|
|
|
|
Session sess = DatabaseSessionFactory.createSession();
|
|
|
|
me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(sess, channelId);
|
|
|
|
if (channel == null) {
|
|
ChannelInfo info;
|
|
|
|
try {
|
|
info = ChannelInfo.getInfo("https://youtube.com/channel/" + channelId);
|
|
} catch (Exception e) {
|
|
return;
|
|
}
|
|
|
|
channel = new me.kavin.piped.utils.obj.db.Channel(channelId, info.getName(),
|
|
info.getAvatarUrl(), info.isVerified());
|
|
sess.save(channel);
|
|
|
|
Multithreading.runAsync(() -> {
|
|
try {
|
|
Session sessSub = DatabaseSessionFactory.createSession();
|
|
subscribePubSub(channelId, sessSub);
|
|
sessSub.close();
|
|
} 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);
|
|
}
|
|
|
|
if (!sess.getTransaction().isActive())
|
|
sess.getTransaction().begin();
|
|
sess.getTransaction().commit();
|
|
}
|
|
|
|
sess.close();
|
|
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static byte[] subscriptionsResponse(String session)
|
|
throws IOException {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
User user = DatabaseHelper.getUserFromSession(s, session);
|
|
|
|
if (user != null) {
|
|
|
|
List<SubscriptionChannel> subscriptionItems = new ObjectArrayList<>();
|
|
|
|
CriteriaBuilder cb = s.getCriteriaBuilder();
|
|
var query = cb.createQuery(me.kavin.piped.utils.obj.db.Channel.class);
|
|
var root = query.from(me.kavin.piped.utils.obj.db.Channel.class);
|
|
var userRoot = query.from(User.class);
|
|
query.select(root);
|
|
query.where(cb.and(
|
|
cb.isMember(root.get("uploader_id"), userRoot.<Collection<String>>get("subscribed_ids")),
|
|
cb.equal(userRoot.get("id"), user.getId())
|
|
));
|
|
|
|
var channels = s.createQuery(query).list();
|
|
|
|
channels.forEach(channel -> subscriptionItems.add(new SubscriptionChannel("/channel/" + channel.getUploaderId(),
|
|
channel.getUploader(), rewriteURL(channel.getUploaderAvatar()), channel.isVerified())));
|
|
|
|
subscriptionItems.sort(Comparator.comparing(o -> o.name));
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(subscriptionItems);
|
|
}
|
|
|
|
s.close();
|
|
|
|
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
|
|
|
}
|
|
|
|
public static String registeredBadgeRedirect() {
|
|
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
|
|
long registered = (Long) s.createQuery("select count(*) from User").uniqueResult();
|
|
|
|
s.close();
|
|
|
|
return String.format("https://img.shields.io/badge/Registered%%20Users-%s-blue", registered);
|
|
}
|
|
|
|
public static void handleNewVideo(String url, long time, me.kavin.piped.utils.obj.db.Channel channel, Session s) {
|
|
try {
|
|
handleNewVideo(StreamInfo.getInfo(url), time, channel, s);
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
}
|
|
|
|
private static void handleNewVideo(StreamInfo info, long time, me.kavin.piped.utils.obj.db.Channel channel, Session s) {
|
|
|
|
if (channel == null)
|
|
channel = DatabaseHelper.getChannelFromId(s,
|
|
info.getUploaderUrl().substring("https://www.youtube.com/channel/".length()));
|
|
|
|
long infoTime = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
|
|
: System.currentTimeMillis();
|
|
|
|
Video video = null;
|
|
|
|
if (channel != null && (video = DatabaseHelper.getVideoFromId(s, info.getId())) == null
|
|
&& (System.currentTimeMillis() - infoTime) < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION)) {
|
|
|
|
video = new Video(info.getId(), info.getName(), info.getViewCount(), info.getDuration(),
|
|
Math.max(infoTime, time), info.getThumbnailUrl(), channel);
|
|
|
|
s.save(video);
|
|
|
|
if (!s.getTransaction().isActive())
|
|
s.getTransaction().begin();
|
|
s.getTransaction().commit();
|
|
} else if (video != null) {
|
|
updateVideo(info.getId(), info, time);
|
|
}
|
|
}
|
|
|
|
private static void updateVideo(String id, StreamInfoItem item, long time, boolean addIfNotExistent) {
|
|
Multithreading.runAsync(() -> {
|
|
try {
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
Video video = DatabaseHelper.getVideoFromId(s, id);
|
|
|
|
if (video != null) {
|
|
updateVideo(s, video, item.getViewCount(), item.getDuration(), item.getName());
|
|
} else if (addIfNotExistent) {
|
|
handleNewVideo("https://www.youtube.com/watch?v=" + id, time, null, s);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void updateVideo(String id, StreamInfo info, long time) {
|
|
Multithreading.runAsync(() -> {
|
|
try {
|
|
Session s = DatabaseSessionFactory.createSession();
|
|
Video video = DatabaseHelper.getVideoFromId(s, id);
|
|
|
|
if (video != null) {
|
|
updateVideo(s, video, info.getViewCount(), info.getDuration(), info.getName());
|
|
} else {
|
|
handleNewVideo(info, time, null, s);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
ExceptionHandler.handle(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void updateVideo(Session s, Video video, long views, long duration, String title) {
|
|
|
|
boolean changed = false;
|
|
|
|
if (duration > 0 && video.getDuration() != duration) {
|
|
video.setDuration(duration);
|
|
changed = true;
|
|
}
|
|
if (!video.getTitle().equals(title)) {
|
|
video.setTitle(title);
|
|
changed = true;
|
|
}
|
|
if (views > video.getViews()) {
|
|
video.setViews(views);
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
s.update(video);
|
|
if (!s.getTransaction().isActive()) s.getTransaction().begin();
|
|
s.getTransaction().commit();
|
|
}
|
|
}
|
|
|
|
public static void subscribePubSub(String channelId, Session s) throws IOException {
|
|
|
|
PubSub pubsub = DatabaseHelper.getPubSubFromId(s, channelId);
|
|
|
|
if (pubsub == null || System.currentTimeMillis() - pubsub.getSubbedAt() > TimeUnit.DAYS.toMillis(4)) {
|
|
|
|
String callback = Constants.PUBLIC_URL + "/webhooks/pubsub";
|
|
String topic = "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + channelId;
|
|
|
|
var builder = new Request.Builder()
|
|
.url("https://pubsubhubbub.appspot.com/subscribe");
|
|
|
|
var formBuilder = new FormBody.Builder();
|
|
|
|
formBuilder.add("hub.callback", callback);
|
|
formBuilder.add("hub.topic", topic);
|
|
formBuilder.add("hub.verify", "async");
|
|
formBuilder.add("hub.mode", "subscribe");
|
|
formBuilder.add("hub.lease_seconds", "432000");
|
|
|
|
var resp = Constants.h2client
|
|
.newCall(builder.post(formBuilder.build())
|
|
.build()).execute();
|
|
|
|
if (resp.code() == 202) {
|
|
if (pubsub == null)
|
|
pubsub = new PubSub(channelId, System.currentTimeMillis());
|
|
else
|
|
pubsub.setSubbedAt(System.currentTimeMillis());
|
|
|
|
s.saveOrUpdate(pubsub);
|
|
|
|
if (!s.getTransaction().isActive())
|
|
s.getTransaction().begin();
|
|
s.getTransaction().commit();
|
|
} else
|
|
System.out.println("Failed to subscribe: " + resp.code() + "\n" + Objects.requireNonNull(resp.body()).string());
|
|
}
|
|
|
|
}
|
|
|
|
private static StreamItem collectRelatedStream(Object o) {
|
|
|
|
StreamInfoItem item = (StreamInfoItem) o;
|
|
|
|
return new StreamItem(substringYouTube(item.getUrl()), item.getName(), rewriteURL(item.getThumbnailUrl()),
|
|
item.getUploaderName(), substringYouTube(item.getUploaderUrl()),
|
|
rewriteURL(item.getUploaderAvatarUrl()), item.getTextualUploadDate(), item.getShortDescription(), item.getDuration(),
|
|
item.getViewCount(), item.getUploadDate() != null ? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli() : -1, item.isUploaderVerified());
|
|
}
|
|
}
|