(search) Add experimental OPML-export function for feed subscriptions

This commit is contained in:
Viktor Lofgren 2025-01-01 17:17:54 +01:00
parent ab5c30ad51
commit 84f55b84ff
3 changed files with 56 additions and 2 deletions

View File

@ -1,6 +1,7 @@
package nu.marginalia.search;
import com.google.inject.Inject;
import io.jooby.Jooby;
import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
import nu.marginalia.WebsiteUrl;
@ -18,6 +19,7 @@ public class SearchService extends JoobyService {
private final WebsiteUrl websiteUrl;
private final StaticResources staticResources;
private final SearchSiteSubscriptionService siteSubscriptionService;
private static final Logger logger = LoggerFactory.getLogger(SearchService.class);
private static final Histogram wmsa_search_service_request_time = Histogram.build()
@ -38,6 +40,7 @@ public class SearchService extends JoobyService {
StaticResources staticResources,
SearchFrontPageService frontPageService,
SearchAddToCrawlQueueService addToCrawlQueueService,
SearchSiteSubscriptionService siteSubscriptionService,
SearchSiteInfoService siteInfoService,
SearchCrosstalkService crosstalkService,
SearchBrowseService searchBrowseService,
@ -57,6 +60,14 @@ public class SearchService extends JoobyService {
this.websiteUrl = websiteUrl;
this.staticResources = staticResources;
this.siteSubscriptionService = siteSubscriptionService;
}
@Override
public void startJooby(Jooby jooby) {
super.startJooby(jooby);
jooby.get("/export-opml", siteSubscriptionService::exportOpml);
}
//
// SearchServiceMetrics.get("/search", searchQueryService::pathSearch);

View File

@ -4,6 +4,8 @@ import com.google.inject.Inject;
import io.jooby.Context;
import io.jooby.Cookie;
import io.jooby.Value;
import nu.marginalia.api.feeds.FeedsClient;
import nu.marginalia.api.feeds.RpcFeed;
import nu.marginalia.db.DbDomainQueries;
import nu.marginalia.model.EdgeDomain;
import org.slf4j.Logger;
@ -12,19 +14,25 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ExecutionException;
public class SearchSiteSubscriptionService {
private final DbDomainQueries dbDomainQueries;
private final FeedsClient feedsClient;
private static final Logger logger = LoggerFactory.getLogger(SearchSiteSubscriptionService.class);
@Inject
public SearchSiteSubscriptionService(DbDomainQueries dbDomainQueries) {
public SearchSiteSubscriptionService(DbDomainQueries dbDomainQueries, FeedsClient feedsClient) {
this.dbDomainQueries = dbDomainQueries;
this.feedsClient = feedsClient;
}
public HashSet<Integer> getSubscriptions(Context context) {
@ -90,4 +98,32 @@ public class SearchSiteSubscriptionService {
putSubscriptions(context, subscriptions);
}
public Object exportOpml(Context ctx) throws ExecutionException, InterruptedException {
ctx.setResponseType("text/xml.opml");
ctx.setResponseHeader("Content-Disposition", "attachment; filename=feeds.opml");
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n");
sb.append("<opml version=\"2.0\">\n");
sb.append("<!-- This is an OPM file that can be imported into many feed readers. See https://opml.org/ for spec. -->\n");
sb.append("<head>\n");
sb.append("<title>Marginalia Subscriptions</title>\n");
sb.append("<dateCreated>").append(LocalDateTime.now().atOffset(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME)).append("</dateCreated>\n");
sb.append("</head>\n");
sb.append("<body>\n");
for (int domainId : getSubscriptions(ctx)) {
RpcFeed feed = feedsClient.getFeed(domainId).get();
sb.append("<outline title=\"")
.append(feed.getDomain())
.append("\" htmlUrl=\"")
.append(new EdgeDomain(feed.getDomain()).toRootUrlHttps().toString())
.append("\" xmlUrl=\"").append(feed.getFeedUrl()).append("\"/>\n");
}
sb.append("</body>\n");
sb.append("</opml>");
return sb.toString();
}
}

View File

@ -81,7 +81,14 @@
</div>
@else
<div class="max-w-7xl mx-auto flex flex-col space-y-4 fill-w m-4">
<div class="my-4 text-black dark:text-white font-serif text-xl mx-8">Subscriptions</div>
<div class="my-4 text-black dark:text-white font-serif text-xl mx-8 place-items-baseline flex">
Subscriptions
<div class="grow"></div>
<a class="text-sm font-sans" href="/export-opml">
<i class="fas fa-download mr-2"></i>
Export as OPML
</a>
</div>
@for (NewsItemCluster cluster : model.news())
!{NewsItem item = cluster.first();}