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.
This commit is contained in:
FireMasterK 2021-07-17 04:10:46 +05:30 committed by GitHub
parent 5ab5635fc4
commit dfca88952a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1324 additions and 163 deletions

View File

@ -6,3 +6,4 @@ Dockerfile
LICENSE
*.md
config.properties
data/

5
.gitignore vendored
View File

@ -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*
hs_err_pid*

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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<String> 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);
}

View File

@ -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);
}

View File

@ -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<String, String> 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);
}

View File

@ -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, "*");
});
}

View File

@ -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<User> cr = cb.createQuery(User.class);
Root<User> 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<User> cr = cb.createQuery(User.class);
Root<User> 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<Channel> cr = cb.createQuery(Channel.class);
Root<Channel> root = cr.from(Channel.class);
cr.select(root).where(root.get("uploader_id").in(id));
return s.createQuery(cr).uniqueResult();
}
public static final List<Video> getVideosFromChannelIds(Session s, List<String> id) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<Video> cr = cb.createQuery(Video.class);
Root<Video> root = cr.from(Video.class);
root.fetch("channel", JoinType.LEFT);
cr.select(root).where(root.get("channel").get("uploader_id").in(id));
return s.createQuery(cr).getResultList();
}
public static final List<String> getGlobalSubscribedChannelIds(Session s) {
@SuppressWarnings("unchecked")
List<String> subscriptions = new ObjectArrayList<>(
new LinkedHashSet<>(s.createNativeQuery("select channel from users_subscribed").getResultList()));
return subscriptions;
}
public static final Video getVideoFromId(Session s, String id) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<Video> cr = cb.createQuery(Video.class);
Root<Video> root = cr.from(Video.class);
cr.select(root).where(root.get("id").in(id));
return s.createQuery(cr).uniqueResult();
}
public static final PubSub getPubSubFromId(Session s, String id) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<PubSub> cr = cb.createQuery(PubSub.class);
Root<PubSub> root = cr.from(PubSub.class);
cr.select(root).where(root.get("id").in(id));
return s.createQuery(cr).uniqueResult();
}
public static final List<PubSub> getPubSubFromIds(Session s, List<String> id) {
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<PubSub> cr = cb.createQuery(PubSub.class);
Root<PubSub> root = cr.from(PubSub.class);
cr.select(root).where(root.get("id").in(id));
return s.createQuery(cr).getResultList();
}
}

View File

@ -0,0 +1,32 @@
package me.kavin.piped.utils;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import me.kavin.piped.consts.Constants;
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 DatabaseSessionFactory {
private static final SessionFactory sessionFactory;
static {
final Configuration configuration = new Configuration();
Constants.hibernateProperties.forEach((key, value) -> configuration.setProperty(key, value));
configuration.setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false");
configuration.configure();
sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Video.class)
.addAnnotatedClass(Channel.class).addAnnotatedClass(PubSub.class).buildSessionFactory();
}
public static final Session createSession() {
return sessionFactory.openSession();
}
}

View File

@ -5,15 +5,25 @@ import java.util.concurrent.Executors;
public class Multithreading {
private static final ExecutorService es = Executors.newFixedThreadPool(16);
private static final ExecutorService es = Executors.newCachedThreadPool();
private static final ExecutorService esLimited = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static final ExecutorService esLimitedPubSub = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public static void runAsync(final Runnable runnable) {
es.submit(runnable);
}
public static void runAsyncTimeout(final Runnable runnable) {
public static void runAsyncLimited(final Runnable runnable) {
esLimited.submit(runnable);
}
es.submit(runnable);
public static void runAsyncLimitedPubSub(final Runnable runnable) {
esLimited.submit(runnable);
}
public static ExecutorService getCachedExecutor() {
return es;
}
}

View File

@ -1,7 +0,0 @@
package me.kavin.piped.utils;
public class RegisterRequest {
public String username, password;
}

View File

@ -6,14 +6,23 @@ import java.net.URI;
import java.net.URL;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.Session;
import org.json.JSONObject;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
@ -33,6 +42,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.github.benmanes.caffeine.cache.Caffeine;
@ -44,6 +54,7 @@ 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 me.kavin.piped.consts.Constants;
import me.kavin.piped.ipfs.IPFS;
@ -51,6 +62,7 @@ import me.kavin.piped.utils.obj.Channel;
import me.kavin.piped.utils.obj.ChapterSegment;
import me.kavin.piped.utils.obj.Comment;
import me.kavin.piped.utils.obj.CommentsPage;
import me.kavin.piped.utils.obj.FeedItem;
import me.kavin.piped.utils.obj.PipedStream;
import me.kavin.piped.utils.obj.Playlist;
import me.kavin.piped.utils.obj.SearchResults;
@ -58,10 +70,19 @@ import me.kavin.piped.utils.obj.StreamItem;
import me.kavin.piped.utils.obj.Streams;
import me.kavin.piped.utils.obj.StreamsPage;
import me.kavin.piped.utils.obj.Subtitle;
import me.kavin.piped.utils.obj.db.PubSub;
import me.kavin.piped.utils.obj.db.User;
import me.kavin.piped.utils.obj.db.Video;
import me.kavin.piped.utils.obj.search.SearchChannel;
import me.kavin.piped.utils.obj.search.SearchItem;
import me.kavin.piped.utils.obj.search.SearchPlaylist;
import me.kavin.piped.utils.obj.search.SearchStream;
import me.kavin.piped.utils.resp.AcceptedResponse;
import me.kavin.piped.utils.resp.AlreadyRegisteredResponse;
import me.kavin.piped.utils.resp.AuthenticationFailureResponse;
import me.kavin.piped.utils.resp.IncorrectCredentialsResponse;
import me.kavin.piped.utils.resp.LoginResponse;
import me.kavin.piped.utils.resp.SubscribeStatusResponse;
public class ResponseHelper {
@ -144,6 +165,11 @@ public class ResponseHelper {
info.getStreamSegments().forEach(
segment -> segments.add(new ChapterSegment(segment.getTitle(), segment.getStartTimeSeconds())));
long time = info.getUploadDate().offsetDateTime().toInstant().toEpochMilli();
if (info.getUploadDate() != null && System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(10))
updateViews(info.getId(), info.getViewCount(), time, false);
final Streams streams = new Streams(info.getName(), info.getDescription().getContent(),
info.getTextualUploadDate(), info.getUploaderName(), info.getUploaderUrl().substring(23),
rewriteURL(info.getUploaderAvatarUrl()), rewriteURL(info.getThumbnailUrl()), info.getDuration(),
@ -167,6 +193,25 @@ public class ResponseHelper {
item.getTextualUploadDate(), item.getDuration(), item.getViewCount()));
});
Multithreading.runAsync(() -> {
Session s = DatabaseSessionFactory.createSession();
me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(s, info.getId());
if (channel != null) {
for (StreamInfoItem item : info.getRelatedItems()) {
long time = item.getUploadDate() != null
? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
if (System.currentTimeMillis() - time < TimeUnit.DAYS.toMillis(10))
updateViews(item.getUrl().substring("https://www.youtube.com/watch?v=".length()),
item.getViewCount(), time, true);
}
}
s.close();
});
String nextpage = null;
if (info.hasNextPage()) {
Page page = info.getNextPage();
@ -461,9 +506,288 @@ public class ResponseHelper {
}
public static final byte[] registerResponse(String user, String pass) throws IOException {
private static final Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
return Constants.mapper.writeValueAsBytes(null);
public static final byte[] registerResponse(String user, String pass)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
user = user.toLowerCase();
Session s = DatabaseSessionFactory.createSession();
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<User> cr = cb.createQuery(User.class);
Root<User> root = cr.from(User.class);
cr.select(root).where(root.get("username").in(user));
boolean registered = s.createQuery(cr).uniqueResult() != null;
if (registered) {
s.close();
return Constants.mapper.writeValueAsBytes(new AlreadyRegisteredResponse());
}
User newuser = new User(user, argon2PasswordEncoder.encode(pass), Collections.emptyList());
s.save(newuser);
s.getTransaction().begin();
s.getTransaction().commit();
s.close();
return Constants.mapper.writeValueAsBytes(new LoginResponse(newuser.getSessionId()));
}
public static final byte[] loginResponse(String user, String pass)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
user = user.toLowerCase();
Session s = DatabaseSessionFactory.createSession();
CriteriaBuilder cb = s.getCriteriaBuilder();
CriteriaQuery<User> cr = cb.createQuery(User.class);
Root<User> root = cr.from(User.class);
cr.select(root).where(root.get("username").in(user));
User dbuser = s.createQuery(cr).uniqueResult();
if (dbuser != null && argon2PasswordEncoder.matches(pass, dbuser.getPassword())) {
s.close();
return Constants.mapper.writeValueAsBytes(new LoginResponse(dbuser.getSessionId()));
}
s.close();
return Constants.mapper.writeValueAsBytes(new IncorrectCredentialsResponse());
}
public static final byte[] subscribeResponse(String session, String channelId)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Session s = DatabaseSessionFactory.createSession();
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, session);
if (user != null) {
if (!user.getSubscribed().contains(channelId)) {
s.getTransaction().begin();
s.createNativeQuery("insert into users_subscribed (subscriber, channel) values (?,?)")
.setParameter(1, user.getId()).setParameter(2, channelId).executeUpdate();
s.getTransaction().commit();
s.close();
Multithreading.runAsync(() -> {
Session sess = DatabaseSessionFactory.createSession();
me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(sess, channelId);
if (channel == null) {
ChannelInfo info = null;
try {
info = ChannelInfo.getInfo("https://youtube.com/channel/" + channelId);
} catch (IOException | ExtractionException e) {
ExceptionUtils.rethrow(e);
}
channel = new me.kavin.piped.utils.obj.db.Channel(channelId, info.getName(),
info.getAvatarUrl(), false);
sess.save(channel);
sess.beginTransaction().commit();
try {
Session sessSub = DatabaseSessionFactory.createSession();
subscribePubSub(channelId, sessSub);
sessSub.close();
} catch (IOException | InterruptedException e) {
ExceptionUtils.rethrow(e);
}
for (StreamInfoItem item : info.getRelatedItems()) {
long time = item.getUploadDate() != null
? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
if ((System.currentTimeMillis() - time) < TimeUnit.DAYS.toMillis(10))
handleNewVideo(item.getUrl(), time, channel, s);
}
}
sess.close();
});
}
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
}
s.close();
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
}
public static final byte[] unsubscribeResponse(String session, String channelId)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Session s = DatabaseSessionFactory.createSession();
User user = DatabaseHelper.getUserFromSession(s, session);
if (user != null) {
s.getTransaction().begin();
s.createNativeQuery("delete from users_subscribed where subscriber = :id and channel = :channel")
.setParameter("id", user.getId()).setParameter("channel", channelId).executeUpdate();
s.getTransaction().commit();
s.close();
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
}
s.close();
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
}
public static final byte[] isSubscribedResponse(String session, String channelId)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Session s = DatabaseSessionFactory.createSession();
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, session);
if (user != null) {
if (user.getSubscribed().contains(channelId)) {
s.close();
return Constants.mapper.writeValueAsBytes(new SubscribeStatusResponse(true));
}
s.close();
return Constants.mapper.writeValueAsBytes(new SubscribeStatusResponse(false));
}
s.close();
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
}
public static final byte[] feedResponse(String session)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Session s = DatabaseSessionFactory.createSession();
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, session);
if (user != null) {
List<FeedItem> feedItems = new ObjectArrayList<>();
if (user.getSubscribed() != null && !user.getSubscribed().isEmpty()) {
List<Video> videos = DatabaseHelper.getVideosFromChannelIds(s, user.getSubscribed());
videos.forEach(video -> {
feedItems.add(new FeedItem(video.getId(), video.getTitle(), rewriteURL(video.getThumbnail()),
video.getChannel().getUploaderId(), video.getChannel().getUploader(),
rewriteURL(video.getChannel().getUploaderAvatar()), video.getViews(), video.getDuration(),
video.getUploaded(), video.getChannel().isVerified()));
});
Collections.sort(feedItems, (a, b) -> (int) (b.uploaded - a.uploaded));
}
s.close();
return Constants.mapper.writeValueAsBytes(feedItems);
}
s.close();
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
}
public static final byte[] importResponse(String session, String[] channelIds)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Session s = DatabaseSessionFactory.createSession();
User user = DatabaseHelper.getUserFromSessionWithSubscribed(s, session);
if (user != null) {
Multithreading.runAsync(() -> {
for (String channelId : channelIds)
if (!user.getSubscribed().contains(channelId))
user.getSubscribed().add(channelId);
if (channelIds.length > 0) {
s.update(user);
s.beginTransaction().commit();
}
s.close();
});
for (String channelId : channelIds) {
Multithreading.runAsyncLimited(() -> {
try {
Session sess = DatabaseSessionFactory.createSession();
me.kavin.piped.utils.obj.db.Channel channel = DatabaseHelper.getChannelFromId(sess, channelId);
if (channel == null) {
ChannelInfo info = null;
try {
info = ChannelInfo.getInfo("https://youtube.com/channel/" + channelId);
} catch (Exception e) {
return;
}
channel = new me.kavin.piped.utils.obj.db.Channel(channelId, info.getName(),
info.getAvatarUrl(), false);
sess.save(channel);
Multithreading.runAsync(() -> {
try {
Session sessSub = DatabaseSessionFactory.createSession();
subscribePubSub(channelId, sessSub);
sessSub.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
for (StreamInfoItem item : info.getRelatedItems()) {
long time = item.getUploadDate() != null
? item.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
if ((System.currentTimeMillis() - time) < TimeUnit.DAYS.toMillis(10))
handleNewVideo(item.getUrl(), time, channel, sess);
}
if (!sess.getTransaction().isActive())
sess.getTransaction().begin();
sess.getTransaction().commit();
}
sess.close();
} catch (Exception e) {
e.printStackTrace();
}
});
}
return Constants.mapper.writeValueAsBytes(new AcceptedResponse());
}
s.close();
return Constants.mapper.writeValueAsBytes(new AuthenticationFailureResponse());
}
@ -486,6 +810,115 @@ public class ResponseHelper {
}
public static void handleNewVideo(String url, long time, me.kavin.piped.utils.obj.db.Channel channel, Session s) {
try {
handleNewVideo(StreamInfo.getInfo(url), time, channel, s);
} catch (IOException | ExtractionException e) {
ExceptionUtils.rethrow(e);
}
}
private static void handleNewVideo(StreamInfo info, long time, me.kavin.piped.utils.obj.db.Channel channel,
Session s) {
if (channel == null)
channel = DatabaseHelper.getChannelFromId(s,
info.getUploaderUrl().substring("https://www.youtube.com/channel/".length()));
long infoTime = info.getUploadDate() != null ? info.getUploadDate().offsetDateTime().toInstant().toEpochMilli()
: System.currentTimeMillis();
Video video = null;
if (channel != null && (video = DatabaseHelper.getVideoFromId(s, info.getId())) == null
&& (System.currentTimeMillis() - infoTime) < TimeUnit.DAYS.toMillis(10)) {
video = new Video(info.getId(), info.getName(), info.getViewCount(), info.getDuration(),
Math.max(infoTime, time), info.getThumbnailUrl(), channel);
s.save(video);
if (!s.getTransaction().isActive())
s.getTransaction().begin();
s.getTransaction().commit();
} else if (video != null) {
video.setViews(info.getViewCount());
s.update(video);
s.getTransaction().commit();
}
}
private static void updateViews(String id, long views, long time, boolean addIfNonExistent) {
Multithreading.runAsync(() -> {
try {
Session s = DatabaseSessionFactory.createSession();
Video video = DatabaseHelper.getVideoFromId(s, id);
if (video != null) {
video.setViews(views);
s.update(video);
s.beginTransaction().commit();
} else if (addIfNonExistent)
handleNewVideo("https://www.youtube.com/watch?v=" + id, time, null, s);
s.close();
} catch (Exception e) {
e.printStackTrace();
}
});
}
public static void subscribePubSub(String channelId, Session s) throws IOException, InterruptedException {
PubSub pubsub = DatabaseHelper.getPubSubFromId(s, channelId);
if (pubsub == null || System.currentTimeMillis() - pubsub.getSubbedAt() > TimeUnit.DAYS.toMillis(4)) {
System.out.println(String.format("PubSub: Subscribing to %s", channelId));
String callback = Constants.PUBLIC_URL + "/webhooks/pubsub";
String topic = "https://www.youtube.com/xml/feeds/videos.xml?channel_id=" + channelId;
Builder builder = HttpRequest.newBuilder(URI.create("https://pubsubhubbub.appspot.com/subscribe"));
Map<String, String> formParams = new Object2ObjectOpenHashMap<>();
StringBuilder formBody = new StringBuilder();
builder.header("content-type", "application/x-www-form-urlencoded");
formParams.put("hub.callback", callback);
formParams.put("hub.topic", topic);
formParams.put("hub.verify", "async");
formParams.put("hub.mode", "subscribe");
formParams.put("hub.lease_seconds", "432000");
formParams.forEach((name, value) -> {
formBody.append(name + "=" + URLUtils.silentEncode(value) + "&");
});
builder.method("POST",
BodyPublishers.ofString(String.valueOf(formBody.substring(0, formBody.length() - 1))));
Constants.h2client.send(builder.build(), BodyHandlers.ofInputStream());
if (pubsub == null)
pubsub = new PubSub(channelId, System.currentTimeMillis());
else
pubsub.setSubbedAt(System.currentTimeMillis());
s.saveOrUpdate(pubsub);
if (!s.getTransaction().isActive())
s.getTransaction().begin();
s.getTransaction().commit();
}
}
private static final String optionalSubstring(String s, int index) {
return s == null || s.isEmpty() ? null : s.substring(index);
}

View File

@ -0,0 +1,24 @@
package me.kavin.piped.utils.obj;
public class FeedItem {
public String id, title, thumbnail, uploader_id, uploader, uploaderAvatar;
public long views, duration, uploaded;
public boolean verified;
public FeedItem(String id, String title, String thumbnail, String uploader_id, String uploader,
String uploaderAvatar, long views, long duration, long uploaded, boolean verified) {
this.id = id;
this.title = title;
this.thumbnail = thumbnail;
this.uploader_id = uploader_id;
this.uploader = uploader;
this.uploaderAvatar = uploaderAvatar;
this.views = views;
this.duration = duration;
this.uploaded = uploaded;
this.verified = verified;
}
}

View File

@ -0,0 +1,67 @@
package me.kavin.piped.utils.obj.db;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
@Entity
@Table(name = "channels", indexes = { @Index(columnList = "uploader_id", name = "channels_uploader_id_idx") })
public class Channel {
@Id
@Column(name = "uploader_id", length = 30)
private String uploader_id;
@Column(name = "uploader", length = 80)
private String uploader;
@Column(name = "uploader_avatar", length = 150)
private String uploaderAvatar;
@Column(name = "verified")
private boolean verified;
public Channel() {
}
public Channel(String uploader_id, String uploader, String uploaderAvatar, boolean verified) {
this.uploader_id = uploader_id;
this.uploader = uploader;
this.uploaderAvatar = uploaderAvatar;
this.verified = verified;
}
public String getUploaderId() {
return uploader_id;
}
public void setUploaderId(String uploader_id) {
this.uploader_id = uploader_id;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public String getUploaderAvatar() {
return uploaderAvatar;
}
public void setUploaderAvatar(String uploaderAvatar) {
this.uploaderAvatar = uploaderAvatar;
}
public boolean isVerified() {
return verified;
}
public void setVerified(boolean verified) {
this.verified = verified;
}
}

View File

@ -0,0 +1,43 @@
package me.kavin.piped.utils.obj.db;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
@Entity
@Table(name = "pubsub", indexes = { @Index(columnList = "id", name = "id_idx") })
public class PubSub {
@Id
@Column(name = "id", unique = true, length = 24)
private String id;
@Column(name = "subbed_at")
private long subbedAt;
public PubSub() {
}
public PubSub(String id, long subbedAt) {
this.id = id;
this.subbedAt = subbedAt;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public long getSubbedAt() {
return subbedAt;
}
public void setSubbedAt(long subbedAt) {
this.subbedAt = subbedAt;
}
}

View File

@ -0,0 +1,93 @@
package me.kavin.piped.utils.obj.db;
import java.io.Serializable;
import java.util.List;
import java.util.UUID;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.Table;
@Entity
@Table(name = "users", indexes = { @Index(columnList = "id", name = "users_id_idx"),
@Index(columnList = "username", name = "username_idx") })
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "username", unique = true, length = 24)
private String username;
@Column(name = "password", columnDefinition = "text")
private String password;
@Column(name = "session_id", length = 36)
private String sessionId;
@ElementCollection
@CollectionTable(name = "users_subscribed", joinColumns = @JoinColumn(name = "subscriber"), indexes = @Index(columnList = "subscriber", name = "subscriber_idx"))
@Column(name = "channel", length = 30)
private List<String> subscribed_ids;
public User() {
}
public User(String username, String password, List<String> subscribed_ids) {
this.username = username;
this.password = password;
this.subscribed_ids = subscribed_ids;
this.sessionId = String.valueOf(UUID.randomUUID());
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public void setPassword(String password) {
this.password = password;
}
public List<String> getSubscribed() {
return subscribed_ids;
}
public void setSubscribed(List<String> subscribed_ids) {
this.subscribed_ids = subscribed_ids;
}
}

View File

@ -0,0 +1,108 @@
package me.kavin.piped.utils.obj.db;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "videos", indexes = { @Index(columnList = "id", name = "videos_id_idx"),
@Index(columnList = "uploader_id", name = "video_uploader_id_idx") })
public class Video {
@Id
@Column(name = "id", unique = true, length = 16)
private String id;
@Column(name = "title", length = 100)
private String title;
@Column(name = "views")
private long views;
@Column(name = "duration")
private long duration;
@Column(name = "uploaded")
private long uploaded;
@Column(name = "thumbnail", length = 150)
private String thumbnail;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "uploader_id")
private Channel channel;
public Video() {
}
public Video(String id, String title, long views, long duration, long uploaded, String thumbnail, Channel channel) {
this.id = id;
this.title = title;
this.views = views;
this.duration = duration;
this.uploaded = uploaded;
this.thumbnail = thumbnail;
this.channel = channel;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public long getViews() {
return views;
}
public void setViews(long views) {
this.views = views;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public long getUploaded() {
return uploaded;
}
public void setUploaded(long uploaded) {
this.uploaded = uploaded;
}
public String getThumbnail() {
return thumbnail;
}
public void setThumbnail(String thumbnail) {
this.thumbnail = thumbnail;
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class AcceptedResponse {
public String message = "ok";
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class AlreadyRegisteredResponse {
public String error = "The username you have used is already taken.";
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class AuthenticationFailureResponse {
public String error = "An invalid Session ID was provided.";
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class IncorrectCredentialsResponse {
public String error = "The username or password you have entered is incorrect.";
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class LoginRequest {
public String username, password;
}

View File

@ -0,0 +1,10 @@
package me.kavin.piped.utils.resp;
public class LoginResponse {
public String token;
public LoginResponse(String token) {
this.token = token;
}
}

View File

@ -0,0 +1,10 @@
package me.kavin.piped.utils.resp;
public class SubscribeStatusResponse {
public boolean subscribed;
public SubscribeStatusResponse(boolean subscribed) {
this.subscribed = subscribed;
}
}

View File

@ -0,0 +1,7 @@
package me.kavin.piped.utils.resp;
public class SubscriptionUpdateRequest {
public String channelId;
}

View File

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- Optional: Show SQL output for debugging -->
<property name="hibernate.show_sql">false</property>
<property name="hibernate.format_sql">true</property>
<property name="hibernate.connection.provider_class">org.hibernate.hikaricp.internal.HikariCPConnectionProvider</property>
<property name="hibernate.connection.handling_mode">DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT</property>
<property name="hibernate.jdbc.batch_size">50</property>
</session-factory>
</hibernate-configuration>