mirror of
https://github.com/TeamPiped/Piped-Backend.git
synced 2024-08-14 23:51:41 +00:00
Merge pull request #406 from TeamPiped/server-handler-refactor
Refactor server code into multiple classes
This commit is contained in:
commit
bbd057e770
15 changed files with 2070 additions and 1844 deletions
|
@ -3,6 +3,7 @@ package me.kavin.piped;
|
|||
import io.activej.inject.Injector;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.server.ServerLauncher;
|
||||
import me.kavin.piped.utils.*;
|
||||
import me.kavin.piped.utils.obj.db.PlaylistVideo;
|
||||
import me.kavin.piped.utils.obj.db.Video;
|
||||
|
@ -82,7 +83,7 @@ public class Main {
|
|||
.parallel()
|
||||
.forEach(id -> Multithreading.runAsyncLimitedPubSub(() -> {
|
||||
try {
|
||||
ResponseHelper.subscribePubSub(id);
|
||||
PubSubHelper.subscribePubSub(id);
|
||||
} catch (IOException e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package me.kavin.piped;
|
||||
package me.kavin.piped.server;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
|
@ -13,6 +13,10 @@ import io.activej.inject.module.AbstractModule;
|
|||
import io.activej.inject.module.Module;
|
||||
import io.activej.launchers.http.MultithreadedHttpServerLauncher;
|
||||
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.UserHandlers;
|
||||
import me.kavin.piped.utils.*;
|
||||
import me.kavin.piped.utils.resp.DeleteUserRequest;
|
||||
import me.kavin.piped.utils.resp.ErrorResponse;
|
||||
|
@ -54,7 +58,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
}
|
||||
})).map(GET, "/config", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.configResponse(), "public, max-age=86400");
|
||||
return getJsonResponse(GenericHandlers.configResponse(), "public, max-age=86400");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -73,7 +77,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
String url = entry.getLinks().get(0).getHref();
|
||||
if (DatabaseHelper.getVideoFromId(StringUtils.substring(url, -11)) != null)
|
||||
continue;
|
||||
ResponseHelper.handleNewVideo(url, entry.getPublishedDate().getTime(), null);
|
||||
VideoHelpers.handleNewVideo(url, entry.getPublishedDate().getTime(), null);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -93,14 +97,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
}
|
||||
})).map(GET, "/streams/:videoId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.streamsResponse(request.getPathParameter("videoId")),
|
||||
return getJsonResponse(StreamHandlers.streamsResponse(request.getPathParameter("videoId")),
|
||||
"public, s-maxage=21540, max-age=30", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/clips/:clipId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.resolveClipId(request.getPathParameter("clipId")),
|
||||
return getJsonResponse(StreamHandlers.resolveClipId(request.getPathParameter("clipId")),
|
||||
"public, max-age=31536000, immutable");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -108,14 +112,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(GET, "/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(
|
||||
ResponseHelper.channelResponse("channel/" + request.getPathParameter("channelId")),
|
||||
ChannelHandlers.channelResponse("channel/" + request.getPathParameter("channelId")),
|
||||
"public, max-age=600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/c/:name", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.channelResponse("c/" + request.getPathParameter("name")),
|
||||
return getJsonResponse(ChannelHandlers.channelResponse("c/" + request.getPathParameter("name")),
|
||||
"public, max-age=600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -123,14 +127,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(GET, "/user/:name", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(
|
||||
ResponseHelper.channelResponse("user/" + request.getPathParameter("name")),
|
||||
ChannelHandlers.channelResponse("user/" + request.getPathParameter("name")),
|
||||
"public, max-age=600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/nextpage/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.channelPageResponse(request.getPathParameter("channelId"),
|
||||
return getJsonResponse(ChannelHandlers.channelPageResponse(request.getPathParameter("channelId"),
|
||||
request.getQueryParameter("nextpage")), "public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -139,9 +143,9 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
String nextpage = request.getQueryParameter("nextpage");
|
||||
if (StringUtils.isEmpty(nextpage))
|
||||
return getJsonResponse(ResponseHelper.channelTabResponse(request.getQueryParameter("data")), "public, max-age=3600", true);
|
||||
return getJsonResponse(ChannelHandlers.channelTabResponse(request.getQueryParameter("data")), "public, max-age=3600", true);
|
||||
else
|
||||
return getJsonResponse(ResponseHelper.channelTabPageResponse(request.getQueryParameter("data"), nextpage), "public, max-age=3600", true);
|
||||
return getJsonResponse(ChannelHandlers.channelTabPageResponse(request.getQueryParameter("data"), nextpage), "public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -150,14 +154,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
var playlistId = request.getPathParameter("playlistId");
|
||||
var cache = StringUtils.isBlank(playlistId) || playlistId.length() != 36 ?
|
||||
"public, max-age=600" : "private";
|
||||
return getJsonResponse(ResponseHelper.playlistResponse(playlistId), cache, true);
|
||||
return getJsonResponse(me.kavin.piped.server.handlers.PlaylistHandlers.playlistResponse(playlistId), cache, true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/nextpage/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(
|
||||
ResponseHelper.playlistPageResponse(request.getPathParameter("playlistId"),
|
||||
me.kavin.piped.server.handlers.PlaylistHandlers.playlistPageResponse(request.getPathParameter("playlistId"),
|
||||
request.getQueryParameter("nextpage")),
|
||||
"public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
|
@ -166,7 +170,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(GET, "/rss/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getRawResponse(
|
||||
ResponseHelper.playlistRSSResponse(request.getPathParameter("playlistId")),
|
||||
me.kavin.piped.server.handlers.PlaylistHandlers.playlistRSSResponse(request.getPathParameter("playlistId")),
|
||||
"application/atom+xml", "public, s-maxage=600");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -174,7 +178,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
// TODO: Replace with opensearch, below, for caching reasons.
|
||||
})).map(GET, "/suggestions", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.suggestionsResponse(request.getQueryParameter("query")),
|
||||
return getJsonResponse(SearchHandlers.suggestionsResponse(request.getQueryParameter("query")),
|
||||
"public, max-age=600");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -182,14 +186,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(GET, "/opensearch/suggestions", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(
|
||||
ResponseHelper.opensearchSuggestionsResponse(request.getQueryParameter("query")),
|
||||
SearchHandlers.opensearchSuggestionsResponse(request.getQueryParameter("query")),
|
||||
"public, max-age=600");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/search", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.searchResponse(request.getQueryParameter("q"),
|
||||
return getJsonResponse(SearchHandlers.searchResponse(request.getQueryParameter("q"),
|
||||
request.getQueryParameter("filter")), "public, max-age=600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -197,7 +201,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(GET, "/nextpage/search", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(
|
||||
ResponseHelper.searchPageResponse(request.getQueryParameter("q"),
|
||||
SearchHandlers.searchPageResponse(request.getQueryParameter("q"),
|
||||
request.getQueryParameter("filter"), request.getQueryParameter("nextpage")),
|
||||
"public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
|
@ -205,21 +209,21 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
}
|
||||
})).map(GET, "/trending", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.trendingResponse(request.getQueryParameter("region")),
|
||||
return getJsonResponse(TrendingHandlers.trendingResponse(request.getQueryParameter("region")),
|
||||
"public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.commentsResponse(request.getPathParameter("videoId")),
|
||||
return getJsonResponse(StreamHandlers.commentsResponse(request.getPathParameter("videoId")),
|
||||
"public, max-age=1200", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/nextpage/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.commentsPageResponse(request.getPathParameter("videoId"),
|
||||
return getJsonResponse(StreamHandlers.commentsPageResponse(request.getPathParameter("videoId"),
|
||||
request.getQueryParameter("nextpage")), "public, max-age=3600", true);
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -228,7 +232,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||
LoginRequest.class);
|
||||
return getJsonResponse(ResponseHelper.registerResponse(body.username, body.password),
|
||||
return getJsonResponse(UserHandlers.registerResponse(body.username, body.password),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -237,7 +241,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||
LoginRequest.class);
|
||||
return getJsonResponse(ResponseHelper.loginResponse(body.username, body.password), "private");
|
||||
return getJsonResponse(UserHandlers.loginResponse(body.username, body.password), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -246,7 +250,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
SubscriptionUpdateRequest body = Constants.mapper
|
||||
.readValue(request.loadBody().getResult().asArray(), SubscriptionUpdateRequest.class);
|
||||
return getJsonResponse(
|
||||
ResponseHelper.subscribeResponse(request.getHeader(AUTHORIZATION), body.channelId),
|
||||
FeedHandlers.subscribeResponse(request.getHeader(AUTHORIZATION), body.channelId),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -256,35 +260,35 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
SubscriptionUpdateRequest body = Constants.mapper
|
||||
.readValue(request.loadBody().getResult().asArray(), SubscriptionUpdateRequest.class);
|
||||
return getJsonResponse(
|
||||
ResponseHelper.unsubscribeResponse(request.getHeader(AUTHORIZATION), body.channelId),
|
||||
FeedHandlers.unsubscribeResponse(request.getHeader(AUTHORIZATION), body.channelId),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/subscribed", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.isSubscribedResponse(request.getHeader(AUTHORIZATION),
|
||||
return getJsonResponse(FeedHandlers.isSubscribedResponse(request.getHeader(AUTHORIZATION),
|
||||
request.getQueryParameter("channelId")), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/feed", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.feedResponse(request.getQueryParameter("authToken")),
|
||||
return getJsonResponse(FeedHandlers.feedResponse(request.getQueryParameter("authToken")),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/feed/rss", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getRawResponse(ResponseHelper.feedResponseRSS(request.getQueryParameter("authToken")),
|
||||
return getRawResponse(FeedHandlers.feedResponseRSS(request.getQueryParameter("authToken")),
|
||||
"application/atom+xml", "public, s-maxage=120");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/feed/unauthenticated", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.unauthenticatedFeedResponse(
|
||||
return getJsonResponse(FeedHandlers.unauthenticatedFeedResponse(
|
||||
Objects.requireNonNull(request.getQueryParameter("channels")).split(",")
|
||||
), "public, s-maxage=120");
|
||||
} catch (Exception e) {
|
||||
|
@ -292,7 +296,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
}
|
||||
})).map(GET, "/feed/unauthenticated/rss", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getRawResponse(ResponseHelper.unauthenticatedFeedResponseRSS(
|
||||
return getRawResponse(FeedHandlers.unauthenticatedFeedResponseRSS(
|
||||
Objects.requireNonNull(request.getQueryParameter("channels")).split(",")
|
||||
), "application/atom+xml", "public, s-maxage=120");
|
||||
} catch (Exception e) {
|
||||
|
@ -302,7 +306,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
String[] subscriptions = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||
String[].class);
|
||||
return getJsonResponse(ResponseHelper.importResponse(request.getHeader(AUTHORIZATION),
|
||||
return getJsonResponse(FeedHandlers.importResponse(request.getHeader(AUTHORIZATION),
|
||||
subscriptions, Boolean.parseBoolean(request.getQueryParameter("override"))), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -311,20 +315,20 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
var json = Constants.mapper.readTree(request.loadBody().getResult().asArray());
|
||||
var playlistId = json.get("playlistId").textValue();
|
||||
return getJsonResponse(ResponseHelper.importPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.importPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/subscriptions", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.subscriptionsResponse(request.getHeader(AUTHORIZATION)),
|
||||
return getJsonResponse(FeedHandlers.subscriptionsResponse(request.getHeader(AUTHORIZATION)),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(GET, "/subscriptions/unauthenticated", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.unauthenticatedSubscriptionsResponse(
|
||||
return getJsonResponse(FeedHandlers.unauthenticatedSubscriptionsResponse(
|
||||
Objects.requireNonNull(request.getQueryParameter("channels")).split(",")
|
||||
), "public, s-maxage=120");
|
||||
} catch (Exception e) {
|
||||
|
@ -333,13 +337,13 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
})).map(POST, "/user/playlists/create", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
var name = Constants.mapper.readTree(request.loadBody().getResult().asArray()).get("name").textValue();
|
||||
return getJsonResponse(ResponseHelper.createPlaylist(request.getHeader(AUTHORIZATION), name), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.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");
|
||||
return getJsonResponse(AuthPlaylistHandlers.playlistsResponse(request.getHeader(AUTHORIZATION)), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -348,7 +352,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
var json = Constants.mapper.readTree(request.loadBody().getResult().asArray());
|
||||
var playlistId = json.get("playlistId").textValue();
|
||||
var videoId = json.get("videoId").textValue();
|
||||
return getJsonResponse(ResponseHelper.addToPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, videoId), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.addToPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, videoId), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -357,7 +361,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
var json = Constants.mapper.readTree(request.loadBody().getResult().asArray());
|
||||
var playlistId = json.get("playlistId").textValue();
|
||||
var index = json.get("index").intValue();
|
||||
return getJsonResponse(ResponseHelper.removeFromPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, index), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.removeFromPlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, index), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -366,7 +370,7 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
var json = Constants.mapper.readTree(request.loadBody().getResult().asArray());
|
||||
var playlistId = json.get("playlistId").textValue();
|
||||
var newName = json.get("newName").textValue();
|
||||
return getJsonResponse(ResponseHelper.renamePlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, newName), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.renamePlaylistResponse(request.getHeader(AUTHORIZATION), playlistId, newName), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
|
@ -374,13 +378,13 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
var json = Constants.mapper.readTree(request.loadBody().getResult().asArray());
|
||||
var playlistId = json.get("playlistId").textValue();
|
||||
return getJsonResponse(ResponseHelper.deletePlaylistResponse(request.getHeader(AUTHORIZATION), playlistId), "private");
|
||||
return getJsonResponse(AuthPlaylistHandlers.deletePlaylistResponse(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, ResponseHelper.registeredBadgeRedirect())
|
||||
return HttpResponse.ofCode(302).withHeader(LOCATION, GenericHandlers.registeredBadgeRedirect())
|
||||
.withHeader(CACHE_CONTROL, "public, max-age=30");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
|
@ -389,14 +393,14 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher {
|
|||
try {
|
||||
DeleteUserRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(),
|
||||
DeleteUserRequest.class);
|
||||
return getJsonResponse(ResponseHelper.deleteUserResponse(request.getHeader(AUTHORIZATION), body.password),
|
||||
return getJsonResponse(UserHandlers.deleteUserResponse(request.getHeader(AUTHORIZATION), body.password),
|
||||
"private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
||||
})).map(POST, "/logout", AsyncServlet.ofBlocking(executor, request -> {
|
||||
try {
|
||||
return getJsonResponse(ResponseHelper.logoutResponse(request.getHeader(AUTHORIZATION)), "private");
|
||||
return getJsonResponse(UserHandlers.logoutResponse(request.getHeader(AUTHORIZATION)), "private");
|
||||
} catch (Exception e) {
|
||||
return getErrorResponse(e, request.getPath());
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.ipfs.IPFS;
|
||||
import me.kavin.piped.utils.*;
|
||||
import me.kavin.piped.utils.obj.*;
|
||||
import me.kavin.piped.utils.obj.db.Video;
|
||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.StatelessSession;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelTabInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YouTubeChannelTabHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
|
||||
import static me.kavin.piped.utils.URLUtils.rewriteURL;
|
||||
|
||||
public class ChannelHandlers {
|
||||
public static byte[] channelResponse(String channelPath) throws Exception {
|
||||
|
||||
final ChannelInfo info = ChannelInfo.getInfo("https://youtube.com/" + channelPath);
|
||||
|
||||
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
|
||||
var channel = DatabaseHelper.getChannelFromId(info.getId());
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
if (channel != null) {
|
||||
|
||||
boolean modified = false;
|
||||
|
||||
if (channel.isVerified() != info.isVerified()) {
|
||||
channel.setVerified(info.isVerified());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!channel.getUploaderAvatar().equals(info.getAvatarUrl())) {
|
||||
channel.setUploaderAvatar(info.getAvatarUrl());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (!channel.getUploader().equals(info.getName())) {
|
||||
channel.setUploader(info.getName());
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
var tr = s.beginTransaction();
|
||||
s.update(channel);
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
Set<String> ids = info.getRelatedItems()
|
||||
.stream()
|
||||
.filter(item -> {
|
||||
long time = item.getUploadDate() != null
|
||||
? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
|
||||
: System.currentTimeMillis();
|
||||
return System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(Constants.FEED_RETENTION);
|
||||
})
|
||||
.map(item -> {
|
||||
try {
|
||||
return YOUTUBE_SERVICE.getStreamLHFactory().getId(item.getUrl());
|
||||
} catch (ParsingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
List<Video> videos = DatabaseHelper.getVideosFromIds(s, ids);
|
||||
|
||||
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());
|
||||
var video = videos.stream()
|
||||
.filter(v -> v.getId().equals(id))
|
||||
.findFirst();
|
||||
if (video.isPresent()) {
|
||||
VideoHelpers.updateVideo(s, video.get(), item);
|
||||
} else {
|
||||
VideoHelpers.handleNewVideo("https://youtube.com/watch?v=" + id, time, channel);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
List<ChannelTab> tabs = info.getTabs()
|
||||
.stream()
|
||||
.map(tab -> {
|
||||
try {
|
||||
return new ChannelTab(tab.getTab().name(), mapper.writeValueAsString(tab));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
final Channel channel = new Channel(info.getId(), info.getName(), rewriteURL(info.getAvatarUrl()),
|
||||
rewriteURL(info.getBannerUrl()), info.getDescription(), info.getSubscriberCount(), info.isVerified(),
|
||||
nextpage, relatedStreams, tabs);
|
||||
|
||||
IPFS.publishData(channel);
|
||||
|
||||
return mapper.writeValueAsBytes(channel);
|
||||
|
||||
}
|
||||
|
||||
public static byte[] channelPageResponse(String channelId, String prevpageStr)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(prevpageStr))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
Page prevpage = mapper.readValue(prevpageStr, Page.class);
|
||||
|
||||
ListExtractor.InfoItemsPage<StreamInfoItem> info = ChannelInfo.getMoreItems(YOUTUBE_SERVICE,
|
||||
"https://youtube.com/channel/" + channelId, prevpage);
|
||||
|
||||
final List<ContentItem> relatedStreams = collectRelatedItems(info.getItems());
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams);
|
||||
|
||||
return mapper.writeValueAsBytes(streamspage);
|
||||
|
||||
}
|
||||
|
||||
public static byte[] channelTabResponse(String data)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(data))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
YouTubeChannelTabHandler tabHandler = mapper.readValue(data, YouTubeChannelTabHandlerMixin.class);
|
||||
|
||||
var info = ChannelTabInfo.getInfo(YOUTUBE_SERVICE, tabHandler);
|
||||
|
||||
List<ContentItem> items = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new ChannelTabData(nextpage, items));
|
||||
}
|
||||
|
||||
public static byte[] channelTabPageResponse(String data, String prevPageStr) throws Exception {
|
||||
|
||||
if (StringUtils.isEmpty(data))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
YouTubeChannelTabHandler tabHandler = mapper.readValue(data, YouTubeChannelTabHandlerMixin.class);
|
||||
|
||||
Page prevPage = mapper.readValue(prevPageStr, Page.class);
|
||||
|
||||
var info = ChannelTabInfo.getMoreItems(YOUTUBE_SERVICE, tabHandler, prevPage);
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
List<ContentItem> items = collectRelatedItems(info.getItems());
|
||||
|
||||
return mapper.writeValueAsBytes(new ChannelTabData(nextpage, items));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.DatabaseSessionFactory;
|
||||
import org.hibernate.StatelessSession;
|
||||
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
|
||||
public class GenericHandlers {
|
||||
|
||||
public static byte[] configResponse() throws Exception {
|
||||
return mapper.writeValueAsBytes(Constants.frontendProperties);
|
||||
}
|
||||
|
||||
public static String registeredBadgeRedirect() {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
long registered = s.createQuery("select count(*) from User", Long.class).uniqueResult();
|
||||
|
||||
return String.format("https://img.shields.io/badge/Registered%%20Users-%s-blue", registered);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndEntryImpl;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
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.server.handlers.auth.AuthPlaylistHandlers;
|
||||
import me.kavin.piped.utils.obj.ContentItem;
|
||||
import me.kavin.piped.utils.obj.Playlist;
|
||||
import me.kavin.piped.utils.obj.StreamsPage;
|
||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
|
||||
import static me.kavin.piped.utils.URLUtils.rewriteURL;
|
||||
import static me.kavin.piped.utils.URLUtils.substringYouTube;
|
||||
|
||||
public class PlaylistHandlers {
|
||||
public static byte[] playlistResponse(String playlistId) throws ExtractionException, IOException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return 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 AuthPlaylistHandlers.playlistPipedResponse(playlistId);
|
||||
|
||||
return playlistYouTubeResponse(playlistId);
|
||||
}
|
||||
|
||||
private static byte[] playlistYouTubeResponse(String playlistId)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
final PlaylistInfo info = PlaylistInfo.getInfo("https://www.youtube.com/playlist?list=" + playlistId);
|
||||
|
||||
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = 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 mapper.writeValueAsBytes(playlist);
|
||||
|
||||
}
|
||||
|
||||
public static byte[] playlistPageResponse(String playlistId, String prevpageStr)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(prevpageStr))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
Page prevpage = mapper.readValue(prevpageStr, Page.class);
|
||||
|
||||
ListExtractor.InfoItemsPage<StreamInfoItem> info = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE,
|
||||
"https://www.youtube.com/playlist?list=" + playlistId, prevpage);
|
||||
|
||||
final List<ContentItem> relatedStreams = collectRelatedItems(info.getItems());
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
final StreamsPage streamspage = new StreamsPage(nextpage, relatedStreams);
|
||||
|
||||
return mapper.writeValueAsBytes(streamspage);
|
||||
|
||||
}
|
||||
|
||||
public static byte[] playlistRSSResponse(String playlistId) throws ExtractionException, IOException, FeedException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return 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 AuthPlaylistHandlers.playlistPipedRSSResponse(playlistId);
|
||||
|
||||
return playlistYouTubeRSSResponse(playlistId);
|
||||
}
|
||||
|
||||
private static byte[] playlistYouTubeRSSResponse(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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import me.kavin.piped.utils.obj.ContentItem;
|
||||
import me.kavin.piped.utils.obj.SearchResults;
|
||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
|
||||
|
||||
public class SearchHandlers {
|
||||
public static byte[] suggestionsResponse(String query)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(query))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
return mapper.writeValueAsBytes(YOUTUBE_SERVICE.getSuggestionExtractor().suggestionList(query));
|
||||
|
||||
}
|
||||
|
||||
public static byte[] opensearchSuggestionsResponse(String query)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(query))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
return 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));
|
||||
|
||||
List<ContentItem> items = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
Page nextpage = info.getNextPage();
|
||||
|
||||
return mapper.writeValueAsBytes(new SearchResults(items,
|
||||
mapper.writeValueAsString(nextpage), info.getSearchSuggestion(), info.isCorrectedSearch()));
|
||||
|
||||
}
|
||||
|
||||
public static byte[] searchPageResponse(String q, String filter, String prevpageStr)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isEmpty(prevpageStr))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
Page prevpage = mapper.readValue(prevpageStr, Page.class);
|
||||
|
||||
ListExtractor.InfoItemsPage<InfoItem> pages = SearchInfo.getMoreItems(YOUTUBE_SERVICE,
|
||||
YOUTUBE_SERVICE.getSearchQHFactory().fromQuery(q, Collections.singletonList(filter), null), prevpage);
|
||||
|
||||
List<ContentItem> items = collectRelatedItems(pages.getItems());
|
||||
|
||||
Page nextpage = pages.getNextPage();
|
||||
|
||||
return mapper
|
||||
.writeValueAsBytes(new SearchResults(items, mapper.writeValueAsString(nextpage)));
|
||||
|
||||
}
|
||||
}
|
249
src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java
Normal file
249
src/main/java/me/kavin/piped/server/handlers/StreamHandlers.java
Normal file
|
@ -0,0 +1,249 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.*;
|
||||
import me.kavin.piped.utils.obj.*;
|
||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||
import me.kavin.piped.utils.resp.VideoResolvedResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import java.util.List;
|
||||
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.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
|
||||
import static me.kavin.piped.utils.URLUtils.*;
|
||||
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 StreamHandlers {
|
||||
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 var futureDislikeRating = Multithreading.supplyAsync(() -> {
|
||||
try {
|
||||
return RydHelper.getDislikeRating(videoId);
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
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.getContent()),
|
||||
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.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(rewriteVideoURL(stream.getContent()),
|
||||
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(rewriteVideoURL(stream.getContent()), String.valueOf(stream.getFormat()),
|
||||
stream.getResolution(), stream.getFormat().getMimeType(), false)));
|
||||
|
||||
info.getAudioStreams()
|
||||
.forEach(stream -> audioStreams.add(new PipedStream(rewriteVideoURL(stream.getContent()),
|
||||
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<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
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))
|
||||
VideoHelpers.updateVideo(info.getId(), info, time);
|
||||
|
||||
String lbryId;
|
||||
|
||||
try {
|
||||
lbryId = futureLbryId.get(2, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
lbryId = null;
|
||||
}
|
||||
|
||||
// Attempt to get dislikes calculating with the RYD API rating
|
||||
if (info.getDislikeCount() < 0 && info.getLikeCount() >= 0) {
|
||||
double rating;
|
||||
try {
|
||||
rating = futureDislikeRating.get(3, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
rating = -1;
|
||||
}
|
||||
|
||||
if (rating > 1 && rating <= 5) {
|
||||
info.setDislikeCount(Math.round(info.getLikeCount() * ((5 - rating) / (rating - 1))));
|
||||
}
|
||||
}
|
||||
|
||||
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, rewriteVideoURL(info.getHlsUrl()),
|
||||
rewriteVideoURL(info.getDashMpdUrl()), lbryId, chapters);
|
||||
|
||||
return 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 mapper.writeValueAsBytes(new VideoResolvedResponse(videoId));
|
||||
}
|
||||
|
||||
public static byte[] commentsResponse(String videoId) throws Exception {
|
||||
|
||||
CommentsInfo info = CommentsInfo.getInfo("https://www.youtube.com/watch?v=" + videoId);
|
||||
|
||||
List<Comment> comments = new ObjectArrayList<>();
|
||||
|
||||
info.getRelatedItems().forEach(comment -> {
|
||||
try {
|
||||
String repliespage = null;
|
||||
if (comment.getReplies() != null)
|
||||
repliespage = 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.getReplyCount(),
|
||||
comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified()));
|
||||
} catch (JsonProcessingException e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
CommentsPage commentsItem = new CommentsPage(comments, nextpage, info.isCommentsDisabled());
|
||||
|
||||
return mapper.writeValueAsBytes(commentsItem);
|
||||
|
||||
}
|
||||
|
||||
public static byte[] commentsPageResponse(String videoId, String prevpageStr) throws Exception {
|
||||
|
||||
if (StringUtils.isEmpty(prevpageStr))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
Page prevpage = mapper.readValue(prevpageStr, Page.class);
|
||||
|
||||
ListExtractor.InfoItemsPage<CommentsInfoItem> info = CommentsInfo.getMoreItems(YOUTUBE_SERVICE, "https://www.youtube.com/watch?v=" + videoId, prevpage);
|
||||
|
||||
List<Comment> comments = new ObjectArrayList<>();
|
||||
|
||||
info.getItems().forEach(comment -> {
|
||||
try {
|
||||
String repliespage = null;
|
||||
if (comment.getReplies() != null)
|
||||
repliespage = 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.getReplyCount(),
|
||||
comment.isHeartedByUploader(), comment.isPinned(), comment.isUploaderVerified()));
|
||||
} catch (JsonProcessingException e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
|
||||
String nextpage = null;
|
||||
if (info.hasNextPage()) {
|
||||
Page page = info.getNextPage();
|
||||
nextpage = mapper.writeValueAsString(page);
|
||||
}
|
||||
|
||||
CommentsPage commentsItem = new CommentsPage(comments, nextpage, false);
|
||||
|
||||
return mapper.writeValueAsBytes(commentsItem);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package me.kavin.piped.server.handlers;
|
||||
|
||||
import me.kavin.piped.utils.obj.ContentItem;
|
||||
import me.kavin.piped.utils.resp.InvalidRequestResponse;
|
||||
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 java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.CollectionUtils.collectRelatedItems;
|
||||
|
||||
public class TrendingHandlers {
|
||||
public static byte[] trendingResponse(String region)
|
||||
throws ExtractionException, IOException {
|
||||
|
||||
if (region == null)
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
|
||||
KioskList kioskList = YOUTUBE_SERVICE.getKioskList();
|
||||
kioskList.forceContentCountry(new ContentCountry(region));
|
||||
KioskExtractor<?> extractor = kioskList.getDefaultKioskExtractor();
|
||||
extractor.fetchPage();
|
||||
KioskInfo info = KioskInfo.getInfo(extractor);
|
||||
|
||||
final List<ContentItem> relatedStreams = collectRelatedItems(info.getRelatedItems());
|
||||
|
||||
return mapper.writeValueAsBytes(relatedStreams);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,410 @@
|
|||
package me.kavin.piped.server.handlers.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.rometools.rome.feed.synd.SyndEntry;
|
||||
import com.rometools.rome.feed.synd.SyndEntryImpl;
|
||||
import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.feed.synd.SyndFeedImpl;
|
||||
import com.rometools.rome.io.FeedException;
|
||||
import com.rometools.rome.io.SyndFeedOutput;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.DatabaseHelper;
|
||||
import me.kavin.piped.utils.DatabaseSessionFactory;
|
||||
import me.kavin.piped.utils.URLUtils;
|
||||
import me.kavin.piped.utils.obj.ContentItem;
|
||||
import me.kavin.piped.utils.obj.Playlist;
|
||||
import me.kavin.piped.utils.obj.StreamItem;
|
||||
import me.kavin.piped.utils.obj.db.Channel;
|
||||
import me.kavin.piped.utils.obj.db.PlaylistVideo;
|
||||
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.InvalidRequestResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.StatelessSession;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static me.kavin.piped.consts.Constants.YOUTUBE_SERVICE;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.URLUtils.rewriteURL;
|
||||
import static me.kavin.piped.utils.URLUtils.substringYouTube;
|
||||
|
||||
public class AuthPlaylistHandlers {
|
||||
public static byte[] playlistPipedResponse(String playlistId) throws IOException {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
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)
|
||||
.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 mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Playlist not found"));
|
||||
|
||||
final List<ContentItem> relatedStreams = new ObjectArrayList<>();
|
||||
|
||||
var videos = pl.getVideos();
|
||||
|
||||
for (var video : videos) {
|
||||
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(), false));
|
||||
}
|
||||
|
||||
final Playlist playlist = new Playlist(pl.getName(), rewriteURL(pl.getThumbnail()), null, null, pl.getOwner().getUsername(),
|
||||
null, null, videos.size(), relatedStreams);
|
||||
|
||||
return mapper.writeValueAsBytes(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] playlistPipedRSSResponse(String playlistId)
|
||||
throws FeedException {
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
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)
|
||||
.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();
|
||||
|
||||
final List<SyndEntry> entries = new ObjectArrayList<>();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("rss_2.0");
|
||||
feed.setTitle(pl.getName());
|
||||
feed.setAuthor(pl.getOwner().getUsername());
|
||||
feed.setDescription(String.format("%s - Piped", pl.getName()));
|
||||
feed.setLink(Constants.FRONTEND_URL + "/playlist?list=" + pl.getPlaylistId());
|
||||
feed.setPublishedDate(new Date());
|
||||
|
||||
for (var video : pl.getVideos()) {
|
||||
SyndEntry entry = new SyndEntryImpl();
|
||||
entry.setAuthor(video.getChannel().getUploader());
|
||||
entry.setLink(Constants.FRONTEND_URL + "/video?id=" + video.getId());
|
||||
entry.setUri(Constants.FRONTEND_URL + "/video?id=" + video.getId());
|
||||
entry.setTitle(video.getTitle());
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
feed.setEntries(entries);
|
||||
|
||||
return new SyndFeedOutput().outputString(feed).getBytes(UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] createPlaylist(String session, String name) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(name))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return 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/");
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.persist(playlist);
|
||||
tr.commit();
|
||||
|
||||
ObjectNode response = mapper.createObjectNode();
|
||||
response.put("playlistId", String.valueOf(playlist.getPlaylistId()));
|
||||
|
||||
return mapper.writeValueAsBytes(response);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] renamePlaylistResponse(String session, String playlistId, String newName) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
var playlist = DatabaseHelper.getPlaylistFromId(s, playlistId);
|
||||
|
||||
if (playlist == null)
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Playlist not found"));
|
||||
|
||||
if (playlist.getOwner().getId() != user.getId())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "You do not own this playlist"));
|
||||
|
||||
playlist.setName(newName);
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.merge(playlist);
|
||||
tr.commit();
|
||||
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
|
||||
public static byte[] deletePlaylistResponse(String session, String playlistId) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
var playlist = DatabaseHelper.getPlaylistFromId(s, playlistId);
|
||||
|
||||
if (playlist == null)
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Playlist not found"));
|
||||
|
||||
if (playlist.getOwner().getId() != user.getId())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "You do not own this playlist"));
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.remove(playlist);
|
||||
tr.commit();
|
||||
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
|
||||
public static byte[] addToPlaylistResponse(String session, String playlistId, String videoId) throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId) || StringUtils.isBlank(videoId))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
var user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return 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);
|
||||
root.fetch("videos", JoinType.LEFT);
|
||||
root.fetch("owner", JoinType.LEFT);
|
||||
query.where(cb.equal(root.get("playlist_id"), UUID.fromString(playlistId)));
|
||||
var playlist = s.createQuery(query).uniqueResult();
|
||||
|
||||
if (playlist == null)
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Playlist not found"));
|
||||
|
||||
if (playlist.getOwner().getId() != user.getId())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "You are not the owner this playlist"));
|
||||
|
||||
var video = DatabaseHelper.getPlaylistVideoFromId(s, videoId);
|
||||
|
||||
if (video == null) {
|
||||
StreamInfo info = StreamInfo.getInfo("https://www.youtube.com/watch?v=" + videoId);
|
||||
|
||||
String channelId = StringUtils.substringAfter(info.getUploaderUrl(), "/channel/");
|
||||
|
||||
var channel = DatabaseHelper.getChannelFromId(s, channelId);
|
||||
|
||||
if (channel == null) {
|
||||
channel = DatabaseHelper.saveChannel(channelId);
|
||||
}
|
||||
|
||||
video = new PlaylistVideo(videoId, info.getName(), info.getThumbnailUrl(), info.getDuration(), channel);
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.persist(video);
|
||||
tr.commit();
|
||||
|
||||
}
|
||||
|
||||
if (playlist.getVideos().isEmpty())
|
||||
playlist.setThumbnail(video.getThumbnail());
|
||||
|
||||
playlist.getVideos().add(video);
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.merge(playlist);
|
||||
tr.commit();
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] removeFromPlaylistResponse(String session, String playlistId, int index) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
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);
|
||||
root.fetch("videos", JoinType.LEFT);
|
||||
root.fetch("owner", JoinType.LEFT);
|
||||
query.where(cb.equal(root.get("playlist_id"), UUID.fromString(playlistId)));
|
||||
var playlist = s.createQuery(query).uniqueResult();
|
||||
|
||||
if (playlist == null)
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Playlist not found"));
|
||||
|
||||
if (playlist.getOwner().getId() != DatabaseHelper.getUserFromSession(session).getId())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "You are not the owner this playlist"));
|
||||
|
||||
if (index < 0 || index >= playlist.getVideos().size())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "Video Index out of bounds"));
|
||||
|
||||
playlist.getVideos().remove(index);
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.merge(playlist);
|
||||
tr.commit();
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] importPlaylistResponse(String session, String playlistId) throws IOException, ExtractionException {
|
||||
|
||||
if (StringUtils.isBlank(playlistId))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
var user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
final String url = "https://www.youtube.com/playlist?list=" + playlistId;
|
||||
|
||||
PlaylistInfo info = PlaylistInfo.getInfo(url);
|
||||
|
||||
var playlist = new me.kavin.piped.utils.obj.db.Playlist(info.getName(), user, info.getThumbnailUrl());
|
||||
|
||||
List<StreamInfoItem> videos = new ObjectArrayList<>(info.getRelatedItems());
|
||||
|
||||
Page nextpage = info.getNextPage();
|
||||
|
||||
while (nextpage != null) {
|
||||
var page = PlaylistInfo.getMoreItems(YOUTUBE_SERVICE, url, nextpage);
|
||||
videos.addAll(page.getItems());
|
||||
|
||||
nextpage = page.getNextPage();
|
||||
}
|
||||
|
||||
Set<String> channelIds = videos.stream()
|
||||
.map(StreamInfoItem::getUploaderUrl)
|
||||
.map(URLUtils::substringYouTube)
|
||||
.map(s -> s.substring("/channel/".length()))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
List<String> videoIds = videos.stream()
|
||||
.map(StreamInfoItem::getUrl)
|
||||
.map(URLUtils::substringYouTube)
|
||||
.map(s -> s.substring("/watch?v=".length()))
|
||||
.toList();
|
||||
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
|
||||
Map<String, Channel> channelMap = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
var channels = DatabaseHelper.getChannelsFromIds(s, channelIds);
|
||||
channelIds.forEach(id -> {
|
||||
var fetched = channels.stream().filter(channel -> channel.getUploaderId().equals(id)).findFirst()
|
||||
.orElseGet(() -> DatabaseHelper.saveChannel(id));
|
||||
channelMap.put(id, fetched);
|
||||
});
|
||||
|
||||
Map<String, PlaylistVideo> videoMap = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
var playlistVideos = DatabaseHelper.getPlaylistVideosFromIds(s, videoIds);
|
||||
videoIds.forEach(id ->
|
||||
playlistVideos.stream().filter(video -> video.getId().equals(id)).findFirst()
|
||||
.ifPresent(playlistVideo -> videoMap.put(id, playlistVideo))
|
||||
);
|
||||
|
||||
videos.forEach(video -> {
|
||||
var channelId = substringYouTube(video.getUploaderUrl()).substring("/channel/".length());
|
||||
var videoId = substringYouTube(video.getUrl()).substring("/watch?v=".length());
|
||||
|
||||
var channel = channelMap.get(channelId);
|
||||
|
||||
playlist.getVideos().add(videoMap.computeIfAbsent(videoId, (key) -> new PlaylistVideo(videoId, video.getName(), video.getThumbnailUrl(), video.getDuration(), channel)));
|
||||
});
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.persist(playlist);
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("playlistId", String.valueOf(playlist.getPlaylistId()))
|
||||
);
|
||||
}
|
||||
|
||||
public static byte[] playlistsResponse(String session) throws IOException {
|
||||
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session, s);
|
||||
|
||||
if (user == null)
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
var playlists = new ObjectArrayList<>();
|
||||
|
||||
// Select user playlists and count the number of videos in each playlist
|
||||
var query = s.createQuery("SELECT p, COUNT(v) FROM Playlist p LEFT JOIN p.videos v WHERE p.owner = :owner GROUP BY p.id", Object[].class);
|
||||
query.setParameter("owner", user);
|
||||
for (Object[] row : query.list()) {
|
||||
var playlist = (me.kavin.piped.utils.obj.db.Playlist) row[0];
|
||||
var videoCount = (long) row[1];
|
||||
|
||||
ObjectNode node = 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()));
|
||||
node.put("videos", videoCount);
|
||||
playlists.add(node);
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(playlists);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,503 @@
|
|||
package me.kavin.piped.server.handlers.auth;
|
||||
|
||||
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 jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.DatabaseHelper;
|
||||
import me.kavin.piped.utils.DatabaseSessionFactory;
|
||||
import me.kavin.piped.utils.ExceptionHandler;
|
||||
import me.kavin.piped.utils.Multithreading;
|
||||
import me.kavin.piped.utils.obj.StreamItem;
|
||||
import me.kavin.piped.utils.obj.SubscriptionChannel;
|
||||
import me.kavin.piped.utils.obj.db.UnauthenticatedSubscription;
|
||||
import me.kavin.piped.utils.obj.db.User;
|
||||
import me.kavin.piped.utils.obj.db.Video;
|
||||
import me.kavin.piped.utils.resp.AcceptedResponse;
|
||||
import me.kavin.piped.utils.resp.AuthenticationFailureResponse;
|
||||
import me.kavin.piped.utils.resp.SubscribeStatusResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.StatelessSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
import static me.kavin.piped.utils.URLUtils.rewriteURL;
|
||||
|
||||
public class FeedHandlers {
|
||||
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)) {
|
||||
|
||||
user.getSubscribed().add(channelId);
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.merge(user);
|
||||
tr.commit();
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
var channel = DatabaseHelper.getChannelFromId(channelId);
|
||||
if (channel == null) {
|
||||
Multithreading.runAsync(() -> DatabaseHelper.saveChannel(channelId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static byte[] isSubscribedResponse(String session, String channelId) throws IOException {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
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 mapper.writeValueAsBytes(new SubscribeStatusResponse(subscribed));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] feedResponse(String session) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(session))
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user != null) {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
CriteriaBuilder cb = s.getCriteriaBuilder();
|
||||
|
||||
// Get all videos from subscribed channels, with channel info
|
||||
CriteriaQuery<Video> criteria = cb.createQuery(Video.class);
|
||||
var root = criteria.from(Video.class);
|
||||
root.fetch("channel", JoinType.INNER);
|
||||
var subquery = criteria.subquery(User.class);
|
||||
var subroot = subquery.from(User.class);
|
||||
subquery.select(subroot.get("subscribed_ids"))
|
||||
.where(cb.equal(subroot.get("id"), user.getId()));
|
||||
|
||||
criteria.select(root)
|
||||
.where(
|
||||
root.get("channel").in(subquery)
|
||||
)
|
||||
.orderBy(cb.desc(root.get("uploaded")));
|
||||
|
||||
List<StreamItem> feedItems = new ObjectArrayList<>();
|
||||
|
||||
for (Video video : s.createQuery(criteria).setTimeout(20).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(), video.isShort()));
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(feedItems);
|
||||
}
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
}
|
||||
|
||||
public static byte[] feedResponseRSS(String session) throws IOException, FeedException {
|
||||
|
||||
if (StringUtils.isBlank(session))
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user != null) {
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
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);
|
||||
var root = criteria.from(Video.class);
|
||||
root.fetch("channel", JoinType.INNER);
|
||||
var subquery = criteria.subquery(User.class);
|
||||
var subroot = subquery.from(User.class);
|
||||
subquery.select(subroot.get("subscribed_ids"))
|
||||
.where(cb.equal(subroot.get("id"), user.getId()));
|
||||
|
||||
criteria.select(root)
|
||||
.where(
|
||||
root.get("channel").in(subquery)
|
||||
)
|
||||
.orderBy(cb.desc(root.get("uploaded")));
|
||||
|
||||
List<Video> videos = s.createQuery(criteria)
|
||||
.setTimeout(20)
|
||||
.setMaxResults(100)
|
||||
.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);
|
||||
|
||||
return new SyndFeedOutput().outputString(feed).getBytes(UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
}
|
||||
|
||||
public static byte[] unauthenticatedFeedResponse(String[] channelIds) throws Exception {
|
||||
|
||||
Set<String> filtered = Arrays.stream(channelIds)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(id -> id.matches("[A-Za-z\\d_-]+"))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
if (filtered.isEmpty())
|
||||
return mapper.writeValueAsBytes(Collections.EMPTY_LIST);
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
CriteriaBuilder cb = s.getCriteriaBuilder();
|
||||
|
||||
// Get all videos from subscribed channels, with channel info
|
||||
CriteriaQuery<Video> criteria = cb.createQuery(Video.class);
|
||||
var root = criteria.from(Video.class);
|
||||
root.fetch("channel", JoinType.INNER);
|
||||
|
||||
criteria.select(root)
|
||||
.where(cb.and(
|
||||
root.get("channel").get("id").in(filtered)
|
||||
))
|
||||
.orderBy(cb.desc(root.get("uploaded")));
|
||||
|
||||
List<StreamItem> feedItems = new ObjectArrayList<>();
|
||||
|
||||
for (Video video : s.createQuery(criteria).setTimeout(20).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(), video.isShort()));
|
||||
}
|
||||
|
||||
updateSubscribedTime(filtered);
|
||||
addMissingChannels(filtered);
|
||||
|
||||
return mapper.writeValueAsBytes(feedItems);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] unauthenticatedFeedResponseRSS(String[] channelIds) throws Exception {
|
||||
|
||||
Set<String> filtered = Arrays.stream(channelIds)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(id -> id.matches("[A-Za-z\\d_-]+"))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
if (filtered.isEmpty())
|
||||
return mapper.writeValueAsBytes(mapper.createObjectNode()
|
||||
.put("error", "No valid channel IDs provided"));
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
CriteriaBuilder cb = s.getCriteriaBuilder();
|
||||
|
||||
// Get all videos from subscribed channels, with channel info
|
||||
CriteriaQuery<Video> criteria = cb.createQuery(Video.class);
|
||||
var root = criteria.from(Video.class);
|
||||
root.fetch("channel", JoinType.INNER);
|
||||
|
||||
criteria.select(root)
|
||||
.where(cb.and(
|
||||
root.get("channel").get("id").in(filtered)
|
||||
))
|
||||
.orderBy(cb.desc(root.get("uploaded")));
|
||||
|
||||
List<Video> videos = s.createQuery(criteria)
|
||||
.setTimeout(20)
|
||||
.setMaxResults(100)
|
||||
.list();
|
||||
|
||||
SyndFeed feed = new SyndFeedImpl();
|
||||
feed.setFeedType("atom_1.0");
|
||||
feed.setTitle("Piped - Feed");
|
||||
feed.setDescription("Piped's RSS unauthenticated subscription feed.");
|
||||
feed.setUri(Constants.FRONTEND_URL + "/feed");
|
||||
feed.setPublishedDate(new Date());
|
||||
|
||||
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);
|
||||
|
||||
updateSubscribedTime(filtered);
|
||||
addMissingChannels(filtered);
|
||||
|
||||
return new SyndFeedOutput().outputString(feed).getBytes(UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateSubscribedTime(Collection<String> channelIds) {
|
||||
Multithreading.runAsync(() -> {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
var cb = s.getCriteriaBuilder();
|
||||
var cu = cb.createCriteriaUpdate(UnauthenticatedSubscription.class);
|
||||
var root = cu.getRoot();
|
||||
cu
|
||||
.set(root.get("subscribedAt"), System.currentTimeMillis())
|
||||
.where(cb.and(
|
||||
root.get("id").in(channelIds),
|
||||
cb.lt(root.get("subscribedAt"), System.currentTimeMillis() - (TimeUnit.DAYS.toMillis(Constants.SUBSCRIPTIONS_EXPIRY) / 2))
|
||||
));
|
||||
s.createMutationQuery(cu).executeUpdate();
|
||||
tr.commit();
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void addMissingChannels(Collection<String> channelIds) {
|
||||
Multithreading.runAsyncLimited(() -> {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
var cb = s.getCriteriaBuilder();
|
||||
|
||||
{
|
||||
var query = cb.createQuery();
|
||||
var root = query.from(UnauthenticatedSubscription.class);
|
||||
query.select(root.get("id"))
|
||||
.where(root.get("id").in(channelIds));
|
||||
|
||||
List<Object> existing = s.createQuery(query).setTimeout(20).list();
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
channelIds.stream()
|
||||
.filter(id -> !existing.contains(id))
|
||||
.map(UnauthenticatedSubscription::new)
|
||||
.forEach(s::insert);
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
{
|
||||
var query = cb.createQuery();
|
||||
var root = query.from(me.kavin.piped.utils.obj.db.Channel.class);
|
||||
query.select(root.get("id"))
|
||||
.where(root.get("id").in(channelIds));
|
||||
|
||||
List<Object> existing = s.createQuery(query).setTimeout(20).list();
|
||||
|
||||
channelIds.stream()
|
||||
.filter(id -> !existing.contains(id))
|
||||
.forEach(id -> Multithreading.runAsyncLimited(() -> DatabaseHelper.saveChannel(id)));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static byte[] importResponse(String session, String[] channelIds, boolean override) throws IOException {
|
||||
|
||||
|
||||
User user = DatabaseHelper.getUserFromSessionWithSubscribed(session);
|
||||
|
||||
if (user != null) {
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
if (override) {
|
||||
user.setSubscribed(Set.of(channelIds));
|
||||
} else {
|
||||
for (String channelId : channelIds)
|
||||
user.getSubscribed().add(channelId);
|
||||
}
|
||||
|
||||
if (channelIds.length > 0) {
|
||||
var tr = s.beginTransaction();
|
||||
s.merge(user);
|
||||
tr.commit();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var channels = DatabaseHelper.getChannelsFromIds(s, Arrays.asList(channelIds));
|
||||
|
||||
Arrays.stream(channelIds).parallel()
|
||||
.filter(channelId ->
|
||||
channels.stream().parallel()
|
||||
.filter(channel -> channel.getUploaderId().equals(channelId))
|
||||
.findFirst().isEmpty()
|
||||
)
|
||||
.forEach(channelId -> Multithreading.runAsyncLimited(() -> DatabaseHelper.saveChannel(channelId)));
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
}
|
||||
|
||||
public static byte[] subscriptionsResponse(String session)
|
||||
throws IOException {
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user != null) {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
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 subquery = query.subquery(User.class);
|
||||
var subroot = subquery.from(User.class);
|
||||
|
||||
subquery.select(subroot.get("subscribed_ids"))
|
||||
.where(cb.equal(subroot.get("id"), user.getId()));
|
||||
|
||||
query.select(root)
|
||||
.where(root.get("uploader_id").in(subquery));
|
||||
|
||||
var channels = s.createQuery(query).list();
|
||||
|
||||
List<SubscriptionChannel> subscriptionItems = channels
|
||||
.stream().parallel()
|
||||
.sorted(Comparator.comparing(me.kavin.piped.utils.obj.db.Channel::getUploader, String.CASE_INSENSITIVE_ORDER))
|
||||
.map(channel -> new SubscriptionChannel("/channel/" + channel.getUploaderId(),
|
||||
channel.getUploader(), rewriteURL(channel.getUploaderAvatar()), channel.isVerified()))
|
||||
.toList();
|
||||
|
||||
return mapper.writeValueAsBytes(subscriptionItems);
|
||||
}
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
}
|
||||
|
||||
public static byte[] unauthenticatedSubscriptionsResponse(String[] channelIds)
|
||||
throws IOException {
|
||||
|
||||
Set<String> filtered = Arrays.stream(channelIds)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.filter(id -> id.matches("[A-Za-z\\d_-]+"))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
|
||||
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);
|
||||
query.select(root);
|
||||
query.where(root.get("uploader_id").in(filtered));
|
||||
|
||||
var channels = s.createQuery(query).list();
|
||||
|
||||
List<SubscriptionChannel> subscriptionItems = channels
|
||||
.stream().parallel()
|
||||
.sorted(Comparator.comparing(me.kavin.piped.utils.obj.db.Channel::getUploader, String.CASE_INSENSITIVE_ORDER))
|
||||
.map(channel -> new SubscriptionChannel("/channel/" + channel.getUploaderId(),
|
||||
channel.getUploader(), rewriteURL(channel.getUploaderAvatar()), channel.isVerified()))
|
||||
.toList();
|
||||
|
||||
return mapper.writeValueAsBytes(subscriptionItems);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] unsubscribeResponse(String session, String channelId)
|
||||
throws IOException {
|
||||
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user != null) {
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
s.createNativeMutationQuery("delete from users_subscribed where subscriber = :id and channel = :channel")
|
||||
.setParameter("id", user.getId()).setParameter("channel", channelId).executeUpdate();
|
||||
tr.commit();
|
||||
return mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package me.kavin.piped.server.handlers.auth;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.DatabaseHelper;
|
||||
import me.kavin.piped.utils.DatabaseSessionFactory;
|
||||
import me.kavin.piped.utils.RequestUtils;
|
||||
import me.kavin.piped.utils.obj.db.User;
|
||||
import me.kavin.piped.utils.resp.*;
|
||||
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.hibernate.StatelessSession;
|
||||
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static me.kavin.piped.consts.Constants.mapper;
|
||||
|
||||
public class UserHandlers {
|
||||
private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
|
||||
private static final BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
|
||||
|
||||
public static byte[] registerResponse(String user, String pass) throws IOException {
|
||||
|
||||
if (Constants.DISABLE_REGISTRATION)
|
||||
return mapper.writeValueAsBytes(new DisabledRegistrationResponse());
|
||||
|
||||
if (StringUtils.isBlank(user) || StringUtils.isBlank(pass))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
if (user.length() > 24)
|
||||
return mapper.writeValueAsBytes(
|
||||
mapper.createObjectNode()
|
||||
.put("error", "The username must be less than 24 characters")
|
||||
);
|
||||
|
||||
user = user.toLowerCase();
|
||||
|
||||
try (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(cb.equal(root.get("username"), user));
|
||||
boolean registered = s.createQuery(cr).uniqueResult() != null;
|
||||
|
||||
if (registered)
|
||||
return 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 mapper.writeValueAsBytes(new CompromisedPasswordResponse());
|
||||
}
|
||||
|
||||
User newuser = new User(user, argon2PasswordEncoder.encode(pass), Set.of());
|
||||
|
||||
var tr = s.beginTransaction();
|
||||
s.persist(newuser);
|
||||
tr.commit();
|
||||
|
||||
|
||||
return mapper.writeValueAsBytes(new LoginResponse(newuser.getSessionId()));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hashMatch(String hash, String pass) {
|
||||
return hash.startsWith("$argon2") ?
|
||||
argon2PasswordEncoder.matches(pass, hash) :
|
||||
bcryptPasswordEncoder.matches(pass, hash);
|
||||
}
|
||||
|
||||
public static byte[] loginResponse(String user, String pass)
|
||||
throws IOException {
|
||||
|
||||
if (user == null || pass == null)
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
user = user.toLowerCase();
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
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 (hashMatch(hash, pass)) {
|
||||
return mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
|
||||
}
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] deleteUserResponse(String session, String pass) throws IOException {
|
||||
|
||||
if (StringUtils.isBlank(pass))
|
||||
return mapper.writeValueAsBytes(new InvalidRequestResponse());
|
||||
|
||||
try (Session s = DatabaseSessionFactory.createSession()) {
|
||||
User user = DatabaseHelper.getUserFromSession(session);
|
||||
|
||||
if (user == null)
|
||||
return mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
|
||||
String hash = user.getPassword();
|
||||
|
||||
if (!hashMatch(hash, pass))
|
||||
return mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
|
||||
|
||||
try {
|
||||
var tr = s.beginTransaction();
|
||||
s.remove(user);
|
||||
tr.commit();
|
||||
} catch (Exception e) {
|
||||
return mapper.writeValueAsBytes(new ErrorResponse(ExceptionUtils.getStackTrace(e), e.getMessage()));
|
||||
}
|
||||
|
||||
return mapper.writeValueAsBytes(new DeleteUserResponse(user.getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] logoutResponse(String session) throws JsonProcessingException {
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
if (s.createMutationQuery("UPDATE User user SET user.sessionId = :newSessionId where user.sessionId = :sessionId")
|
||||
.setParameter("sessionId", session).setParameter("newSessionId", String.valueOf(UUID.randomUUID()))
|
||||
.executeUpdate() > 0) {
|
||||
tr.commit();
|
||||
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
|
||||
} else
|
||||
tr.rollback();
|
||||
}
|
||||
|
||||
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
|
||||
}
|
||||
}
|
|
@ -4,13 +4,20 @@ import jakarta.persistence.criteria.CriteriaBuilder;
|
|||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.obj.db.*;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.hibernate.SharedSessionContract;
|
||||
import org.hibernate.StatelessSession;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DatabaseHelper {
|
||||
|
||||
|
@ -130,4 +137,51 @@ public class DatabaseHelper {
|
|||
return getPubSubFromId(s, id);
|
||||
}
|
||||
}
|
||||
|
||||
public static Channel saveChannel(String channelId) {
|
||||
|
||||
if (!channelId.matches("[A-Za-z\\d_-]+"))
|
||||
return null;
|
||||
|
||||
|
||||
final ChannelInfo info;
|
||||
|
||||
try {
|
||||
info = ChannelInfo.getInfo("https://youtube.com/channel/" + channelId);
|
||||
} catch (IOException | ExtractionException e) {
|
||||
ExceptionUtils.rethrow(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
var channel = new Channel(channelId, info.getName(),
|
||||
info.getAvatarUrl(), info.isVerified());
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
s.insert(channel);
|
||||
tr.commit();
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
try {
|
||||
PubSubHelper.subscribePubSub(channelId);
|
||||
} catch (IOException e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
|
||||
Multithreading.runAsync(() -> {
|
||||
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))
|
||||
VideoHelpers.handleNewVideo(item.getUrl(), time, channel);
|
||||
}
|
||||
});
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
|
58
src/main/java/me/kavin/piped/utils/PubSubHelper.java
Normal file
58
src/main/java/me/kavin/piped/utils/PubSubHelper.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
package me.kavin.piped.utils;
|
||||
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.obj.db.PubSub;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import org.hibernate.StatelessSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PubSubHelper {
|
||||
public static void subscribePubSub(String channelId) throws IOException {
|
||||
|
||||
PubSub pubsub = DatabaseHelper.getPubSubFromId(channelId);
|
||||
|
||||
if (pubsub == null || System.currentTimeMillis() - pubsub.getSubbedAt() > TimeUnit.DAYS.toMillis(4)) {
|
||||
|
||||
String callback = Constants.PUBSUB_URL + "/webhooks/pubsub";
|
||||
String topic = "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + channelId;
|
||||
|
||||
var builder = new Request.Builder()
|
||||
.url(Constants.PUBSUB_HUB_URL);
|
||||
|
||||
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");
|
||||
|
||||
try (var resp = Constants.h2client
|
||||
.newCall(builder.post(formBuilder.build())
|
||||
.build()).execute()) {
|
||||
|
||||
if (resp.code() == 202) {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
if (pubsub == null) {
|
||||
pubsub = new PubSub(channelId, System.currentTimeMillis());
|
||||
s.insert(pubsub);
|
||||
} else {
|
||||
pubsub.setSubbedAt(System.currentTimeMillis());
|
||||
s.update(pubsub);
|
||||
}
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
} else
|
||||
System.out.println("Failed to subscribe: " + resp.code() + "\n" + Objects.requireNonNull(resp.body()).string());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
113
src/main/java/me/kavin/piped/utils/VideoHelpers.java
Normal file
113
src/main/java/me/kavin/piped/utils/VideoHelpers.java
Normal file
|
@ -0,0 +1,113 @@
|
|||
package me.kavin.piped.utils;
|
||||
|
||||
import me.kavin.piped.consts.Constants;
|
||||
import me.kavin.piped.utils.obj.db.Video;
|
||||
import org.hibernate.StatelessSession;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class VideoHelpers {
|
||||
public static void handleNewVideo(String url, long time, me.kavin.piped.utils.obj.db.Channel channel) {
|
||||
try {
|
||||
handleNewVideo(StreamInfo.getInfo(url), time, channel);
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleNewVideo(StreamInfo info, long time, me.kavin.piped.utils.obj.db.Channel channel) {
|
||||
|
||||
if (channel == null)
|
||||
channel = DatabaseHelper.getChannelFromId(
|
||||
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(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(), info.isShortFormContent(), channel);
|
||||
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
var tr = s.beginTransaction();
|
||||
s.insert(video);
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
} else if (video != null) {
|
||||
updateVideo(info.getId(), info, time);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateVideo(String id, StreamInfoItem item, long time, boolean addIfNotExistent) {
|
||||
Multithreading.runAsync(() -> {
|
||||
try {
|
||||
Video video = DatabaseHelper.getVideoFromId(id);
|
||||
|
||||
if (video != null) {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
updateVideo(s, video, item.getViewCount(), item.getDuration(), item.getName());
|
||||
}
|
||||
} else if (addIfNotExistent) {
|
||||
handleNewVideo("https://www.youtube.com/watch?v=" + id, time, null);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void updateVideo(String id, StreamInfo info, long time) {
|
||||
Multithreading.runAsync(() -> {
|
||||
try {
|
||||
Video video = DatabaseHelper.getVideoFromId(id);
|
||||
|
||||
if (video != null) {
|
||||
try (StatelessSession s = DatabaseSessionFactory.createStatelessSession()) {
|
||||
updateVideo(s, video, info.getViewCount(), info.getDuration(), info.getName());
|
||||
}
|
||||
} else {
|
||||
handleNewVideo(info, time, null);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
ExceptionHandler.handle(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void updateVideo(StatelessSession s, Video video, StreamInfoItem item) {
|
||||
updateVideo(s, video, item.getViewCount(), item.getDuration(), item.getName());
|
||||
}
|
||||
|
||||
public static void updateVideo(StatelessSession 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) {
|
||||
var tr = s.beginTransaction();
|
||||
s.update(video);
|
||||
tr.commit();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue