From dfca88952a49bef500172631d1ac95644ad907aa Mon Sep 17 00:00:00 2001 From: FireMasterK <20838718+FireMasterK@users.noreply.github.com> Date: Sat, 17 Jul 2021 04:10:46 +0530 Subject: [PATCH] Use Hibernate as a database (#32) * Basic hibernate structure. * Commit stash. * Improve a lot of things. * Implement Feed, PubSub, query optimizations. * Update video views on channel visits. * Allow mass importing subscriptions. * Allow configuring database in config.properties. * Major multi-threading and pubsub improvements. * PubSub query improvements and fix for postgres. * Revert docker-compose change. * Disable showing sql statements. --- .dockerignore | 1 + .gitignore | 5 +- build.gradle | 7 +- config.properties | 27 +- docker-compose.yml | 26 +- src/main/java/me/kavin/piped/Main.java | 71 +++ .../java/me/kavin/piped/ServerLauncher.java | 332 ++++++++----- .../java/me/kavin/piped/consts/Constants.java | 15 +- .../piped/utils/CustomServletDecorator.java | 4 +- .../me/kavin/piped/utils/DatabaseHelper.java | 94 ++++ .../piped/utils/DatabaseSessionFactory.java | 32 ++ .../me/kavin/piped/utils/Multithreading.java | 16 +- .../me/kavin/piped/utils/RegisterRequest.java | 7 - .../me/kavin/piped/utils/ResponseHelper.java | 437 +++++++++++++++++- .../me/kavin/piped/utils/obj/FeedItem.java | 24 + .../me/kavin/piped/utils/obj/db/Channel.java | 67 +++ .../me/kavin/piped/utils/obj/db/PubSub.java | 43 ++ .../me/kavin/piped/utils/obj/db/User.java | 93 ++++ .../me/kavin/piped/utils/obj/db/Video.java | 108 +++++ .../piped/utils/resp/AcceptedResponse.java | 7 + .../utils/resp/AlreadyRegisteredResponse.java | 7 + .../resp/AuthenticationFailureResponse.java | 7 + .../resp/IncorrectCredentialsResponse.java | 7 + .../kavin/piped/utils/resp/LoginRequest.java | 7 + .../kavin/piped/utils/resp/LoginResponse.java | 10 + .../utils/resp/SubscribeStatusResponse.java | 10 + .../utils/resp/SubscriptionUpdateRequest.java | 7 + src/main/resources/hibernate.cfg.xml | 16 + 28 files changed, 1324 insertions(+), 163 deletions(-) create mode 100644 src/main/java/me/kavin/piped/utils/DatabaseHelper.java create mode 100644 src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java delete mode 100644 src/main/java/me/kavin/piped/utils/RegisterRequest.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/FeedItem.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/Channel.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/PubSub.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/User.java create mode 100644 src/main/java/me/kavin/piped/utils/obj/db/Video.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/AcceptedResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/AlreadyRegisteredResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/AuthenticationFailureResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/IncorrectCredentialsResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/LoginRequest.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/LoginResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/SubscribeStatusResponse.java create mode 100644 src/main/java/me/kavin/piped/utils/resp/SubscriptionUpdateRequest.java create mode 100644 src/main/resources/hibernate.cfg.xml diff --git a/.dockerignore b/.dockerignore index b493384..5b65182 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ Dockerfile LICENSE *.md config.properties +data/ diff --git a/.gitignore b/.gitignore index f2fc8dd..ca75516 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,11 @@ bin/ ### Gradle ### /build/ +# Database Data +/data/ + # TxT File *.txt # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* \ No newline at end of file +hs_err_pid* diff --git a/build.gradle b/build.gradle index 63c52b8..cbcbe59 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,6 @@ dependencies { implementation 'it.unimi.dsi:fastutil-core:8.5.4' implementation 'commons-codec:commons-codec:1.15' implementation 'org.bouncycastle:bcprov-jdk15on:1.69' - implementation 'org.mongodb:mongodb-driver-sync:4.2.2' implementation 'com.github.TiA4f8R.NewPipeExtractor:NewPipeExtractor:df53170023ff5b97ad1d7ccc74bfce74c3998bcc' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.fasterxml.jackson.core:jackson-core:2.12.4' @@ -32,7 +31,11 @@ dependencies { implementation 'io.activej:activej-boot:4.3' implementation 'io.activej:activej-specializer:4.3' implementation 'io.activej:activej-launchers-http:4.3' - implementation 'net.java.dev.jna:jna-platform:5.8.0' + implementation 'org.postgresql:postgresql:42.2.19' + implementation 'org.hibernate:hibernate-core:5.4.30.Final' + implementation 'org.hibernate:hibernate-hikaricp:5.4.30.Final' + implementation 'org.springframework.security:spring-security-crypto:5.5.1' + implementation 'commons-logging:commons-logging:1.2' } shadowJar { diff --git a/config.properties b/config.properties index 255546a..ae67038 100644 --- a/config.properties +++ b/config.properties @@ -1,13 +1,22 @@ - # The port to Listen on. PORT: 8080 -# The number of workers to use for the server -HTTP_WORKERS: 2 - -# Proxy -PROXY_PART: https://pipedproxy-ams.kavin.rocks +# The number of workers to use for the server +HTTP_WORKERS: 2 -# Captcha Parameters -CAPTCHA_BASE_URL: https://api.capmonster.cloud/ -CAPTCHA_API_KEY: INSERT_HERE +# Proxy +PROXY_PART: https://pipedproxy-ams.kavin.rocks + +# Captcha Parameters +CAPTCHA_BASE_URL: https://api.capmonster.cloud/ +CAPTCHA_API_KEY: INSERT_HERE + +# Public API URL +API_URL: https://pipedapi.kavin.rocks + +# Hibernate properties +hibernate.connection.url: jdbc:postgresql://postgres:5432/piped +hibernate.connection.driver_class: org.postgresql.Driver +hibernate.dialect: org.hibernate.dialect.PostgreSQL10Dialect +hibernate.connection.username: piped +hibernate.connection.password: changeme diff --git a/docker-compose.yml b/docker-compose.yml index 859d04f..16b9a2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,19 @@ -version: "3.8" services: - piped: - image: 1337kavin/piped:latest - restart: unless-stopped - ports: - - "127.0.0.1:8080:8080" - volumes: - - ./config.properties:/app/config.properties + piped: + image: 1337kavin/piped:latest + restart: unless-stopped + ports: + - "127.0.0.1:8080:8080" + volumes: + - ./config.properties:/app/config.properties + depends_on: + - postgres + postgres: + image: postgres:13-alpine + restart: unless-stopped + volumes: + - ./data/db:/var/lib/postgresql/data + environment: + - POSTGRES_DB=piped + - POSTGRES_USER=piped + - POSTGRES_PASSWORD=changeme diff --git a/src/main/java/me/kavin/piped/Main.java b/src/main/java/me/kavin/piped/Main.java index 62e83c4..a2837fb 100644 --- a/src/main/java/me/kavin/piped/Main.java +++ b/src/main/java/me/kavin/piped/Main.java @@ -1,10 +1,24 @@ package me.kavin.piped; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.query.Query; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.Localization; import io.activej.inject.Injector; +import me.kavin.piped.utils.DatabaseHelper; +import me.kavin.piped.utils.DatabaseSessionFactory; import me.kavin.piped.utils.DownloaderImpl; +import me.kavin.piped.utils.Multithreading; +import me.kavin.piped.utils.ResponseHelper; public class Main { @@ -14,6 +28,63 @@ public class Main { Injector.useSpecializer(); + new Timer().scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + Session s = DatabaseSessionFactory.createSession(); + + List channels = DatabaseHelper.getGlobalSubscribedChannelIds(s); + + DatabaseHelper.getPubSubFromIds(s, channels).forEach(pubsub -> { + if (System.currentTimeMillis() - pubsub.getSubbedAt() < TimeUnit.DAYS.toMillis(4)) + channels.remove(pubsub.getId()); + }); + + Collections.shuffle(channels); + + for (String channelId : channels) + Multithreading.runAsyncLimitedPubSub(() -> { + Session sess = DatabaseSessionFactory.createSession(); + try { + ResponseHelper.subscribePubSub(channelId, sess); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + sess.close(); + }); + + s.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 0, TimeUnit.MINUTES.toMillis(90)); + + new Timer().scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + Session s = DatabaseSessionFactory.createSession(); + + Transaction tr = s.getTransaction(); + + tr.begin(); + + Query query = s.createQuery("delete from Video where uploaded < :time").setParameter("time", + System.currentTimeMillis() - TimeUnit.DAYS.toMillis(10)); + + System.out.println(String.format("Cleanup: Removed %o old videos", query.executeUpdate())); + + tr.commit(); + + s.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 0, TimeUnit.MINUTES.toMillis(60)); + new ServerLauncher().launch(args); } diff --git a/src/main/java/me/kavin/piped/ServerLauncher.java b/src/main/java/me/kavin/piped/ServerLauncher.java index 19cb45f..4336d30 100644 --- a/src/main/java/me/kavin/piped/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/ServerLauncher.java @@ -1,17 +1,20 @@ package me.kavin.piped; import static io.activej.config.converter.ConfigConverters.ofInetSocketAddress; +import static io.activej.http.HttpHeaders.AUTHORIZATION; import static io.activej.http.HttpHeaders.CACHE_CONTROL; import static io.activej.http.HttpHeaders.CONTENT_TYPE; +import static io.activej.http.HttpMethod.GET; +import static io.activej.http.HttpMethod.POST; import java.io.ByteArrayInputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hibernate.Session; import org.jetbrains.annotations.NotNull; import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; @@ -32,148 +35,225 @@ import io.activej.inject.module.Module; import io.activej.launchers.http.MultithreadedHttpServerLauncher; import me.kavin.piped.consts.Constants; import me.kavin.piped.utils.CustomServletDecorator; +import me.kavin.piped.utils.DatabaseSessionFactory; +import me.kavin.piped.utils.Multithreading; import me.kavin.piped.utils.ResponseHelper; import me.kavin.piped.utils.SponsorBlockUtils; import me.kavin.piped.utils.resp.ErrorResponse; +import me.kavin.piped.utils.resp.LoginRequest; +import me.kavin.piped.utils.resp.SubscriptionUpdateRequest; public class ServerLauncher extends MultithreadedHttpServerLauncher { @Provides Executor executor() { - return Executors.newCachedThreadPool(); + return Multithreading.getCachedExecutor(); } @Provides AsyncServlet mainServlet(Executor executor) { - RoutingServlet router = RoutingServlet.create().map(HttpMethod.GET, "/webhooks/pubsub", request -> { - return HttpResponse.ok200().withPlainText(request.getQueryParameter("hub.challenge")); - }).map(HttpMethod.POST, "/webhooks/pubsub", AsyncServlet.ofBlocking(executor, request -> { - try { + RoutingServlet router = RoutingServlet.create() + .map(HttpMethod.OPTIONS, "/*", request -> HttpResponse.ofCode(200)) + .map(GET, "/webhooks/pubsub", request -> { + return HttpResponse.ok200().withPlainText(request.getQueryParameter("hub.challenge")); + }).map(POST, "/webhooks/pubsub", AsyncServlet.ofBlocking(executor, request -> { + try { - SyndFeed feed = new SyndFeedInput() - .build(new InputSource(new ByteArrayInputStream(request.loadBody().getResult().asArray()))); + SyndFeed feed = new SyndFeedInput().build( + new InputSource(new ByteArrayInputStream(request.loadBody().getResult().asArray()))); - feed.getEntries().forEach(entry -> { - System.out.println(entry.getLinks().get(0).getHref()); - System.out.println(entry.getAuthors().get(0).getUri()); - }); + Multithreading.runAsync(() -> { + Session s = DatabaseSessionFactory.createSession(); + feed.getEntries().forEach(entry -> { + System.out.println(entry.getLinks().get(0).getHref()); + ResponseHelper.handleNewVideo(entry.getLinks().get(0).getHref(), + entry.getPublishedDate().getTime(), null, s); + }); + s.close(); + }); - return HttpResponse.ofCode(204); + return HttpResponse.ofCode(204); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/sponsors/:videoId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(SponsorBlockUtils - .getSponsors(request.getPathParameter("videoId"), request.getQueryParameter("category")) - .getBytes(StandardCharsets.UTF_8), "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/streams/:videoId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.streamsResponse(request.getPathParameter("videoId")), - "public, s-maxage=21540"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse( - ResponseHelper.channelResponse("channel/" + request.getPathParameter("channelId")), - "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/c/:name", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.channelResponse("c/" + request.getPathParameter("name")), - "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/user/:name", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.channelResponse("user/" + request.getPathParameter("name")), - "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/nextpage/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.channelPageResponse(request.getPathParameter("channelId"), - request.getQueryParameter("nextpage")), "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.playlistResponse(request.getPathParameter("playlistId")), - "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/nextpage/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.playlistPageResponse(request.getPathParameter("playlistId"), - request.getQueryParameter("nextpage")), "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/rss/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.playlistRSSResponse(request.getPathParameter("playlistId")), - "public, s-maxage=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/suggestions", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.suggestionsResponse(request.getQueryParameter("query")), - "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/search", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.searchResponse(request.getQueryParameter("q"), - request.getQueryParameter("filter")), "public, max-age=600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/nextpage/search", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse( - ResponseHelper.searchPageResponse(request.getQueryParameter("q"), - request.getQueryParameter("filter"), request.getQueryParameter("nextpage")), - "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/trending", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.trendingResponse(request.getQueryParameter("region")), - "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.commentsResponse(request.getPathParameter("videoId")), - "public, max-age=1200"); - } catch (Exception e) { - return getErrorResponse(e); - } - })).map("/nextpage/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> { - try { - return getJsonResponse(ResponseHelper.commentsPageResponse(request.getPathParameter("videoId"), - request.getQueryParameter("url")), "public, max-age=3600"); - } catch (Exception e) { - return getErrorResponse(e); - } - })); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/sponsors/:videoId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + SponsorBlockUtils.getSponsors(request.getPathParameter("videoId"), + request.getQueryParameter("category")).getBytes(StandardCharsets.UTF_8), + "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/streams/:videoId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.streamsResponse(request.getPathParameter("videoId")), + "public, s-maxage=21540"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + ResponseHelper.channelResponse("channel/" + request.getPathParameter("channelId")), + "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/c/:name", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.channelResponse("c/" + request.getPathParameter("name")), + "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/user/:name", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + ResponseHelper.channelResponse("user/" + request.getPathParameter("name")), + "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/nextpage/channel/:channelId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.channelPageResponse(request.getPathParameter("channelId"), + request.getQueryParameter("nextpage")), "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.playlistResponse(request.getPathParameter("playlistId")), + "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/nextpage/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + ResponseHelper.playlistPageResponse(request.getPathParameter("playlistId"), + request.getQueryParameter("nextpage")), + "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/rss/playlists/:playlistId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + ResponseHelper.playlistRSSResponse(request.getPathParameter("playlistId")), + "public, s-maxage=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/suggestions", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.suggestionsResponse(request.getQueryParameter("query")), + "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/search", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.searchResponse(request.getQueryParameter("q"), + request.getQueryParameter("filter")), "public, max-age=600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/nextpage/search", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse( + ResponseHelper.searchPageResponse(request.getQueryParameter("q"), + request.getQueryParameter("filter"), request.getQueryParameter("nextpage")), + "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/trending", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.trendingResponse(request.getQueryParameter("region")), + "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.commentsResponse(request.getPathParameter("videoId")), + "public, max-age=1200"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/nextpage/comments/:videoId", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.commentsPageResponse(request.getPathParameter("videoId"), + request.getQueryParameter("url")), "public, max-age=3600"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(POST, "/register", AsyncServlet.ofBlocking(executor, request -> { + try { + LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(), + LoginRequest.class); + return getJsonResponse(ResponseHelper.registerResponse(body.username, body.password), + "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(POST, "/login", AsyncServlet.ofBlocking(executor, request -> { + try { + LoginRequest body = Constants.mapper.readValue(request.loadBody().getResult().asArray(), + LoginRequest.class); + return getJsonResponse(ResponseHelper.loginResponse(body.username, body.password), "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(POST, "/subscribe", AsyncServlet.ofBlocking(executor, request -> { + try { + SubscriptionUpdateRequest body = Constants.mapper + .readValue(request.loadBody().getResult().asArray(), SubscriptionUpdateRequest.class); + return getJsonResponse( + ResponseHelper.subscribeResponse(request.getHeader(AUTHORIZATION), body.channelId), + "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(POST, "/unsubscribe", AsyncServlet.ofBlocking(executor, request -> { + try { + SubscriptionUpdateRequest body = Constants.mapper + .readValue(request.loadBody().getResult().asArray(), SubscriptionUpdateRequest.class); + return getJsonResponse( + ResponseHelper.unsubscribeResponse(request.getHeader(AUTHORIZATION), body.channelId), + "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/subscribed", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.isSubscribedResponse(request.getHeader(AUTHORIZATION), + request.getQueryParameter("channelId")), "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(GET, "/feed", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.feedResponse(request.getQueryParameter("authToken")), + "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })).map(POST, "/import", AsyncServlet.ofBlocking(executor, request -> { + try { + String[] subscriptions = Constants.mapper.readValue(request.loadBody().getResult().asArray(), + String[].class); + return getJsonResponse( + ResponseHelper.importResponse(request.getHeader(AUTHORIZATION), subscriptions), + "private"); + } catch (Exception e) { + return getErrorResponse(e); + } + })); return new CustomServletDecorator(router); } diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 05c811a..50695ab 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -11,8 +11,8 @@ import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import com.fasterxml.jackson.databind.ObjectMapper; -import com.mongodb.client.MongoClient; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import me.kavin.piped.utils.PageMixin; public class Constants { @@ -30,16 +30,18 @@ public class Constants { public static final StreamingService YOUTUBE_SERVICE; + public static final String PUBLIC_URL; + public static final HttpClient h2client = HttpClient.newBuilder().followRedirects(Redirect.NORMAL) .version(Version.HTTP_2).build(); public static final HttpClient h2_no_redir_client = HttpClient.newBuilder().followRedirects(Redirect.NEVER) .version(Version.HTTP_2).build(); // public static final HttpClient h3client = Http3ClientBuilder.newBuilder().followRedirects(Redirect.NORMAL).build(); - public static final MongoClient mongoClient; - public static final ObjectMapper mapper = new ObjectMapper().addMixIn(Page.class, PageMixin.class); + public static final Object2ObjectOpenHashMap hibernateProperties = new Object2ObjectOpenHashMap<>(); + static { Properties prop = new Properties(); try { @@ -51,7 +53,12 @@ public class Constants { PROXY_PART = prop.getProperty("PROXY_PART"); CAPTCHA_BASE_URL = prop.getProperty("CAPTCHA_BASE_URL"); CAPTCHA_API_KEY = prop.getProperty("CAPTCHA_API_KEY"); - mongoClient = null/* MongoClients.create(prop.getProperty("MONGO_URI")) */; + PUBLIC_URL = prop.getProperty("API_URL"); + prop.forEach((_key, _value) -> { + String key = String.valueOf(_key), value = String.valueOf(_value); + if (key.startsWith("hibernate")) + hibernateProperties.put(key, value); + }); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/me/kavin/piped/utils/CustomServletDecorator.java b/src/main/java/me/kavin/piped/utils/CustomServletDecorator.java index 459399c..d0ec91f 100644 --- a/src/main/java/me/kavin/piped/utils/CustomServletDecorator.java +++ b/src/main/java/me/kavin/piped/utils/CustomServletDecorator.java @@ -1,5 +1,6 @@ package me.kavin.piped.utils; +import static io.activej.http.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; import static io.activej.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; import org.jetbrains.annotations.NotNull; @@ -29,7 +30,8 @@ public class CustomServletDecorator implements AsyncServlet { HttpHeaderValue headerValue = HttpHeaderValue.of("app;dur=" + (System.nanoTime() - before) / 1000000.0); - return response.withHeader(HEADER, headerValue).withHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + return response.withHeader(HEADER, headerValue).withHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .withHeader(ACCESS_CONTROL_ALLOW_HEADERS, "*"); }); } diff --git a/src/main/java/me/kavin/piped/utils/DatabaseHelper.java b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java new file mode 100644 index 0000000..7fb1ce7 --- /dev/null +++ b/src/main/java/me/kavin/piped/utils/DatabaseHelper.java @@ -0,0 +1,94 @@ +package me.kavin.piped.utils; + +import java.util.LinkedHashSet; +import java.util.List; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Root; + +import org.hibernate.Session; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import me.kavin.piped.utils.obj.db.Channel; +import me.kavin.piped.utils.obj.db.PubSub; +import me.kavin.piped.utils.obj.db.User; +import me.kavin.piped.utils.obj.db.Video; + +public class DatabaseHelper { + + public static final User getUserFromSession(Session s, String session) { + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(User.class); + Root root = cr.from(User.class); + cr.select(root).where(root.get("sessionId").in(session)); + + return s.createQuery(cr).uniqueResult(); + } + + public static final User getUserFromSessionWithSubscribed(Session s, String session) { + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(User.class); + Root root = cr.from(User.class); + root.fetch("subscribed_ids", JoinType.LEFT); + cr.select(root).where(root.get("sessionId").in(session)); + + return s.createQuery(cr).uniqueResult(); + } + + public static final Channel getChannelFromId(Session s, String id) { + CriteriaBuilder cb = s.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(Channel.class); + Root root = cr.from(Channel.class); + cr.select(root).where(root.get("uploader_id").in(id)); + + return s.createQuery(cr).uniqueResult(); + } + + public static final List