diff --git a/config.properties b/config.properties index d270d37..d746de6 100644 --- a/config.properties +++ b/config.properties @@ -29,6 +29,8 @@ DISABLE_RYD:false DISABLE_SERVER:false # Disable the inclusion of LBRY streams DISABLE_LBRY:false +# How long should unauthenticated subscriptions last for +SUBSCRIPTIONS_EXPIRY:30 # Hibernate properties hibernate.connection.url:jdbc:postgresql://postgres:5432/piped hibernate.connection.driver_class:org.postgresql.Driver diff --git a/src/main/java/me/kavin/piped/Main.java b/src/main/java/me/kavin/piped/Main.java index 77a361c..8fa084b 100644 --- a/src/main/java/me/kavin/piped/Main.java +++ b/src/main/java/me/kavin/piped/Main.java @@ -5,10 +5,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import me.kavin.piped.consts.Constants; import me.kavin.piped.utils.*; -import me.kavin.piped.utils.obj.db.PlaylistVideo; -import me.kavin.piped.utils.obj.db.PubSub; -import me.kavin.piped.utils.obj.db.User; -import me.kavin.piped.utils.obj.db.Video; +import me.kavin.piped.utils.obj.db.*; import org.hibernate.Session; import org.hibernate.StatelessSession; import org.schabi.newpipe.extractor.NewPipe; @@ -69,10 +66,17 @@ public class Main { CriteriaQuery criteria = cb.createQuery(PubSub.class); var root = criteria.from(PubSub.class); var userRoot = criteria.from(User.class); + var subquery = criteria.subquery(UnauthenticatedSubscription.class); + var subRoot = subquery.from(UnauthenticatedSubscription.class); + subquery.select(subRoot.get("id")) + .where(cb.gt(subRoot.get("subscribedAt"), System.currentTimeMillis() - TimeUnit.DAYS.toMillis(Constants.SUBSCRIPTIONS_EXPIRY))); criteria.select(root) - .where(cb.and( - cb.lessThan(root.get("subbedAt"), System.currentTimeMillis() - TimeUnit.DAYS.toMillis(4)), - cb.isMember(root.get("id"), userRoot.>get("subscribed_ids")) + .where(cb.or( + cb.and( + cb.lessThan(root.get("subbedAt"), System.currentTimeMillis() - TimeUnit.DAYS.toMillis(4)), + cb.isMember(root.get("id"), userRoot.>get("subscribed_ids")) + ), + root.get("id").in(subquery) )); List pubSubList = s.createQuery(criteria).list(); diff --git a/src/main/java/me/kavin/piped/ServerLauncher.java b/src/main/java/me/kavin/piped/ServerLauncher.java index 8f080f0..0e48dff 100644 --- a/src/main/java/me/kavin/piped/ServerLauncher.java +++ b/src/main/java/me/kavin/piped/ServerLauncher.java @@ -266,6 +266,22 @@ public class ServerLauncher extends MultithreadedHttpServerLauncher { } catch (Exception e) { return getErrorResponse(e, request.getPath()); } + })).map(GET, "/feed/unauthenticated", AsyncServlet.ofBlocking(executor, request -> { + try { + return getJsonResponse(ResponseHelper.unauthenticatedFeedResponse( + Objects.requireNonNull(request.getQueryParameter("channels")).split(",") + ), "public, s-maxage=120"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } + })).map(GET, "/feed/unauthenticated/rss", AsyncServlet.ofBlocking(executor, request -> { + try { + return getRawResponse(ResponseHelper.unauthenticatedFeedResponseRSS( + Objects.requireNonNull(request.getQueryParameter("channels")).split(",") + ), "application/atom+xml", "public, s-maxage=120"); + } catch (Exception e) { + return getErrorResponse(e, request.getPath()); + } })).map(POST, "/import", AsyncServlet.ofBlocking(executor, request -> { try { String[] subscriptions = Constants.mapper.readValue(request.loadBody().getResult().asArray(), diff --git a/src/main/java/me/kavin/piped/consts/Constants.java b/src/main/java/me/kavin/piped/consts/Constants.java index 90bd387..056d20f 100644 --- a/src/main/java/me/kavin/piped/consts/Constants.java +++ b/src/main/java/me/kavin/piped/consts/Constants.java @@ -59,6 +59,8 @@ public class Constants { public static final boolean DISABLE_LBRY; + public static final int SUBSCRIPTIONS_EXPIRY; + public static final String VERSION; public static final ObjectMapper mapper = new ObjectMapper().addMixIn(Page.class, PageMixin.class); @@ -90,6 +92,7 @@ public class Constants { DISABLE_RYD = Boolean.parseBoolean(getProperty(prop, "DISABLE_RYD", "false")); DISABLE_SERVER = Boolean.parseBoolean(getProperty(prop, "DISABLE_SERVER", "false")); DISABLE_LBRY = Boolean.parseBoolean(getProperty(prop, "DISABLE_LBRY", "false")); + SUBSCRIPTIONS_EXPIRY = Integer.parseInt(getProperty(prop, "SUBSCRIPTIONS_EXPIRY", "30")); System.getenv().forEach((key, value) -> { if (key.startsWith("hibernate")) hibernateProperties.put(key, value); diff --git a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java index 3a250a6..21ef8f3 100644 --- a/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java +++ b/src/main/java/me/kavin/piped/utils/DatabaseSessionFactory.java @@ -20,7 +20,7 @@ public class DatabaseSessionFactory { sessionFactory = configuration.addAnnotatedClass(User.class).addAnnotatedClass(Channel.class) .addAnnotatedClass(Video.class).addAnnotatedClass(PubSub.class).addAnnotatedClass(Playlist.class) - .addAnnotatedClass(PlaylistVideo.class).buildSessionFactory(); + .addAnnotatedClass(PlaylistVideo.class).addAnnotatedClass(UnauthenticatedSubscription.class).buildSessionFactory(); } public static Session createSession() { diff --git a/src/main/java/me/kavin/piped/utils/ResponseHelper.java b/src/main/java/me/kavin/piped/utils/ResponseHelper.java index 8ce860a..8a6e57c 100644 --- a/src/main/java/me/kavin/piped/utils/ResponseHelper.java +++ b/src/main/java/me/kavin/piped/utils/ResponseHelper.java @@ -15,11 +15,10 @@ import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Root; import me.kavin.piped.consts.Constants; import me.kavin.piped.ipfs.IPFS; +import me.kavin.piped.utils.obj.Channel; +import me.kavin.piped.utils.obj.Playlist; import me.kavin.piped.utils.obj.*; -import me.kavin.piped.utils.obj.db.PlaylistVideo; -import me.kavin.piped.utils.obj.db.PubSub; -import me.kavin.piped.utils.obj.db.User; -import me.kavin.piped.utils.obj.db.Video; +import me.kavin.piped.utils.obj.db.*; import me.kavin.piped.utils.obj.search.SearchChannel; import me.kavin.piped.utils.obj.search.SearchPlaylist; import me.kavin.piped.utils.resp.*; @@ -959,6 +958,178 @@ public class ResponseHelper { return mapper.writeValueAsBytes(new AuthenticationFailureResponse()); } + public static byte[] unauthenticatedFeedResponse(String[] channelIds) throws Exception { + + Set 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