Parsing podcast:funding tag, showing payment, funding links on the show info screen (#4933)

This commit is contained in:
Tony Tam 2021-04-06 09:15:14 -07:00 committed by GitHub
parent ef41704166
commit 79c79efce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 269 additions and 26 deletions

View File

@ -6,9 +6,12 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.syndication.namespace.PodcastIndex;
import de.danoeh.antennapod.core.util.DateUtils;
/**
@ -66,8 +69,11 @@ public class Rss2Generator implements FeedGenerator {
xml.endTag(null, "image");
}
if (feed.getPaymentLink() != null) {
GeneratorUtil.addPaymentLink(xml, feed.getPaymentLink(), true);
ArrayList<FeedFunding> fundingList = feed.getPaymentLinks();
if (fundingList != null) {
for (FeedFunding funding: fundingList) {
GeneratorUtil.addPaymentLink(xml, funding.url, true);
}
}
// Write FeedItem data
@ -107,8 +113,14 @@ public class Rss2Generator implements FeedGenerator {
xml.attribute(null, "type", item.getMedia().getMime_type());
xml.endTag(null, "enclosure");
}
if (item.getPaymentLink() != null) {
GeneratorUtil.addPaymentLink(xml, item.getPaymentLink(), true);
if (fundingList != null) {
for (FeedFunding funding: fundingList) {
xml.startTag(PodcastIndex.NSTAG, "funding");
xml.attribute(PodcastIndex.NSTAG, "url", funding.url);
xml.text(funding.content);
GeneratorUtil.addPaymentLink(xml, funding.url, true);
xml.endTag(PodcastIndex.NSTAG, "funding");
}
}
xml.endTag(null, "item");

View File

@ -33,10 +33,14 @@ import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.snackbar.Snackbar;
import com.joanzapata.iconify.Iconify;
import org.apache.commons.lang3.StringUtils;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.storage.DBReader;
@ -58,6 +62,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@ -80,6 +86,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
private TextView txtvPodcastTime;
private TextView txtvPodcastSpace;
private TextView txtvPodcastEpisodeCount;
private TextView txtvFundingUrl;
private TextView lblSupport;
private Button btnvOpenStatistics;
private TextView txtvUrl;
private TextView txtvAuthorHeader;
@ -155,6 +163,8 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
txtvPodcastTime = root.findViewById(R.id.txtvPodcastTime);
btnvOpenStatistics = root.findViewById(R.id.btnvOpenStatistics);
txtvUrl = root.findViewById(R.id.txtvUrl);
lblSupport = root.findViewById(R.id.lblSupport);
txtvFundingUrl = root.findViewById(R.id.txtvFundingUrl);
txtvUrl.setOnClickListener(copyUrlToClipboard);
@ -233,6 +243,29 @@ public class FeedInfoFragment extends Fragment implements Toolbar.OnMenuItemClic
}
txtvUrl.setText(feed.getDownload_url() + " {fa-paperclip}");
if (feed.getPaymentLinks() == null || feed.getPaymentLinks().size() == 0) {
lblSupport.setVisibility(View.GONE);
txtvFundingUrl.setVisibility(View.GONE);
} else {
lblSupport.setVisibility(View.VISIBLE);
ArrayList<FeedFunding> fundingList = feed.getPaymentLinks();
StringBuilder str = new StringBuilder();
HashSet<String> seen = new HashSet<String>();
for (FeedFunding funding : fundingList) {
if (seen.contains(funding.url)) {
continue;
}
seen.add(funding.url);
str.append(funding.content.isEmpty()
? getContext().getResources().getString(R.string.support_podcast)
: funding.content).append(" ").append(funding.url);
str.append("\n");
}
str = new StringBuilder(StringUtils.trim(str.toString()));
txtvFundingUrl.setText(str.toString());
}
Iconify.addIcons(txtvUrl);
refreshToolbarState();
}

View File

@ -90,6 +90,28 @@
tools:background="@android:color/holo_green_dark"
tools:text="http://www.example.com/feed" />
<TextView
android:id="@+id/lblSupport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:text="@string/support_funding_label"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
tools:background="@android:color/holo_red_light" />
<TextView
android:id="@+id/txtvFundingUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="8"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:linksClickable="true"
android:autoLink="web"
tools:background="@android:color/holo_green_dark" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -107,6 +107,7 @@ dependencies {
playApi "com.google.android.support:wearable:$wearableSupportVersion"
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
testImplementation 'androidx.test:core:1.2.0'
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-inline:3.5.13'

View File

@ -54,7 +54,7 @@ public class Feed extends FeedFile {
*/
private String lastUpdate;
private String paymentLink;
private ArrayList<FeedFunding> fundingList;
/**
* Feed type, for example RSS 2 or Atom.
*/
@ -103,8 +103,9 @@ public class Feed extends FeedFile {
/**
* This constructor is used for restoring a feed from the database.
*/
public Feed(long id, String lastUpdate, String title, String customTitle, String link, String description, String paymentLink,
String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl,
public Feed(long id, String lastUpdate, String title, String customTitle, String link,
String description, String paymentLinks, String author, String language,
String type, String feedIdentifier, String imageUrl, String fileUrl,
String downloadUrl, boolean downloaded, boolean paged, String nextPageLink,
String filter, @Nullable SortOrder sortOrder, boolean lastUpdateFailed) {
super(fileUrl, downloadUrl, downloaded);
@ -114,7 +115,7 @@ public class Feed extends FeedFile {
this.lastUpdate = lastUpdate;
this.link = link;
this.description = description;
this.paymentLink = paymentLink;
this.fundingList = FeedFunding.extractPaymentLinks(paymentLinks);
this.author = author;
this.language = language;
this.type = type;
@ -237,8 +238,8 @@ public class Feed extends FeedFile {
if (other.author != null) {
author = other.author;
}
if (other.paymentLink != null) {
paymentLink = other.paymentLink;
if (other.fundingList != null) {
fundingList = other.fundingList;
}
// this feed's nextPage might already point to a higher page, so we only update the nextPage value
// if this feed is not paged and the other feed is.
@ -285,8 +286,8 @@ public class Feed extends FeedFile {
return true;
}
}
if (other.paymentLink != null) {
if (paymentLink == null || !paymentLink.equals(other.paymentLink)) {
if (other.fundingList != null) {
if (fundingList == null || !fundingList.equals(other.fundingList)) {
return true;
}
}
@ -390,12 +391,15 @@ public class Feed extends FeedFile {
this.feedIdentifier = feedIdentifier;
}
public String getPaymentLink() {
return paymentLink;
public void addPayment(FeedFunding funding) {
if (fundingList == null) {
fundingList = new ArrayList<FeedFunding>();
}
fundingList.add(funding);
}
public void setPaymentLink(String paymentLink) {
this.paymentLink = paymentLink;
public ArrayList<FeedFunding> getPaymentLinks() {
return fundingList;
}
public String getLanguage() {

View File

@ -0,0 +1,91 @@
package de.danoeh.antennapod.core.feed;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
public class FeedFunding {
public static final String FUNDING_ENTRIES_SEPARATOR = "\u001e";
public static final String FUNDING_TITLE_SEPARATOR = "\u001f";
public String url;
public String content;
public FeedFunding(String url, String content) {
this.url = url;
this.content = content;
}
public void setContent(String content) {
this.content = content;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !obj.getClass().equals(this.getClass())) {
return false;
}
FeedFunding funding = (FeedFunding) obj;
if (url == null && funding.url == null && content == null && funding.content == null) {
return true;
}
if (url != null && url.equals(funding.url) && content != null && content.equals(funding.content)) {
return true;
}
return false;
}
@Override
public int hashCode() {
return (url + FUNDING_TITLE_SEPARATOR + content).hashCode();
}
public static ArrayList<FeedFunding> extractPaymentLinks(String payLinks) {
if (StringUtils.isBlank(payLinks)) {
return null;
}
// old format before we started with PodcastIndex funding tag
ArrayList<FeedFunding> funding = new ArrayList<FeedFunding>();
if (!payLinks.contains(FeedFunding.FUNDING_ENTRIES_SEPARATOR)
&& !payLinks.contains(FeedFunding.FUNDING_TITLE_SEPARATOR)) {
funding.add(new FeedFunding(payLinks, ""));
return funding;
}
String [] list = payLinks.split(FeedFunding.FUNDING_ENTRIES_SEPARATOR);
if (list.length == 0) {
return null;
}
for (String str : list) {
String [] linkContent = str.split(FeedFunding.FUNDING_TITLE_SEPARATOR);
if (StringUtils.isBlank(linkContent[0])) {
continue;
}
String url = linkContent[0];
String title = "";
if (linkContent.length > 1 && ! StringUtils.isBlank(linkContent[1])) {
title = linkContent[1];
}
funding.add(new FeedFunding(url, title));
}
return funding;
}
public static String getPaymentLinksAsString(ArrayList<FeedFunding> fundingList) {
StringBuilder result = new StringBuilder();
if (fundingList == null) {
return null;
}
for (FeedFunding fund : fundingList) {
result.append(fund.url).append(FeedFunding.FUNDING_TITLE_SEPARATOR).append(fund.content);
result.append(FeedFunding.FUNDING_ENTRIES_SEPARATOR);
}
return StringUtils.removeEnd(result.toString(), FeedFunding.FUNDING_ENTRIES_SEPARATOR);
}
}

View File

@ -17,6 +17,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery;
import org.apache.commons.io.FileUtils;
@ -403,7 +404,7 @@ public class PodDBAdapter {
values.put(KEY_TITLE, feed.getFeedTitle());
values.put(KEY_LINK, feed.getLink());
values.put(KEY_DESCRIPTION, feed.getDescription());
values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
values.put(KEY_PAYMENT_LINK, FeedFunding.getPaymentLinksAsString(feed.getPaymentLinks()));
values.put(KEY_AUTHOR, feed.getAuthor());
values.put(KEY_LANGUAGE, feed.getLanguage());
values.put(KEY_IMAGE_URL, feed.getImageUrl());

View File

@ -7,6 +7,7 @@ import java.util.Map;
import java.util.Stack;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.syndication.namespace.Namespace;
import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
@ -28,6 +29,7 @@ public class HandlerState {
final Map<String, String> alternateUrls;
private final ArrayList<FeedItem> items;
private FeedItem currentItem;
private FeedFunding currentFunding;
final Stack<SyndElement> tagstack;
/**
* Namespaces that have been defined so far.
@ -78,6 +80,14 @@ public class HandlerState {
this.currentItem = currentItem;
}
public FeedFunding getCurrentFunding() {
return currentFunding;
}
public void setCurrentFunding(FeedFunding currentFunding) {
this.currentFunding = currentFunding;
}
/**
* Returns the SyndElement that comes after the top element of the tagstack.
*/

View File

@ -14,6 +14,7 @@ import de.danoeh.antennapod.core.syndication.namespace.NSMedia;
import de.danoeh.antennapod.core.syndication.namespace.NSRSS20;
import de.danoeh.antennapod.core.syndication.namespace.NSSimpleChapters;
import de.danoeh.antennapod.core.syndication.namespace.Namespace;
import de.danoeh.antennapod.core.syndication.namespace.PodcastIndex;
import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom;
@ -107,9 +108,13 @@ class SyndHandler extends DefaultHandler {
&& prefix.equals(NSDublinCore.NSTAG)) {
state.namespaces.put(uri, new NSDublinCore());
Log.d(TAG, "Recognized DublinCore namespace");
} else if (uri.equals(PodcastIndex.NSURI) || uri.equals(PodcastIndex.NSURI2)
&& prefix.equals(PodcastIndex.NSTAG)) {
state.namespaces.put(uri, new PodcastIndex());
Log.d(TAG, "Recognized PodcastIndex namespace");
}
}
}
}
}
private Namespace getHandlingNamespace(String uri, String qName) {
Namespace handler = state.namespaces.get(uri);

View File

@ -0,0 +1,38 @@
package de.danoeh.antennapod.core.syndication.namespace;
import org.jsoup.helper.StringUtil;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.syndication.handler.HandlerState;
public class PodcastIndex extends Namespace {
public static final String NSTAG = "podcast";
public static final String NSURI = "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md";
public static final String NSURI2 = "https://podcastindex.org/namespace/1.0";
private static final String URL = "url";
private static final String FUNDING = "funding";
@Override
public SyndElement handleElementStart(String localName, HandlerState state,
Attributes attributes) {
if (FUNDING.equals(localName)) {
String href = attributes.getValue(URL);
FeedFunding funding = new FeedFunding(href, "");
state.setCurrentFunding(funding);
state.getFeed().addPayment(state.getCurrentFunding());
}
return new SyndElement(localName, this);
}
@Override
public void handleElementEnd(String localName, HandlerState state) {
if (state.getContentBuf() == null) {
return;
}
String content = state.getContentBuf().toString();
if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !StringUtil.isBlank(content)) {
state.getCurrentFunding().setContent(content);
}
}
}

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.syndication.namespace.atom;
import android.text.TextUtils;
import android.util.Log;
import de.danoeh.antennapod.core.feed.FeedFunding;
import de.danoeh.antennapod.core.syndication.util.SyndStringUtils;
import org.xml.sax.Attributes;
@ -137,7 +138,7 @@ public class NSAtom extends Namespace {
//A Link such as to a directory such as iTunes
}
} else if (LINK_REL_PAYMENT.equals(rel) && state.getFeed() != null) {
state.getFeed().setPaymentLink(href);
state.getFeed().addPayment(new FeedFunding(href, ""));
} else if (LINK_REL_NEXT.equals(rel) && state.getFeed() != null) {
state.getFeed().setPaged(true);
state.getFeed().setNextPageLink(href);

View File

@ -88,6 +88,8 @@
<string name="author_label">Author(s)</string>
<string name="language_label">Language</string>
<string name="url_label">URL</string>
<string name="support_funding_label">Support</string>
<string name="support_podcast">Support this Podcast</string>
<string name="cover_label">Cover</string>
<string name="error_label">Error</string>
<string name="error_msg_prefix">An error occurred:</string>

View File

@ -48,7 +48,7 @@ public class FeedCursorMapperTest {
assertEquals("feed custom title", feed.getCustomTitle());
assertEquals("feed link", feed.getLink());
assertEquals("feed description", feed.getDescription());
assertEquals("feed payment link", feed.getPaymentLink());
assertEquals("feed payment link", feed.getPaymentLinks().get(0).url);
assertEquals("feed author", feed.getAuthor());
assertEquals("feed language", feed.getLanguage());
assertEquals("feed image url", feed.getImageUrl());

View File

@ -30,7 +30,7 @@ public class AtomParserTest {
assertEquals("http://example.com/feed", feed.getFeedIdentifier());
assertEquals("http://example.com", feed.getLink());
assertEquals("This is the description", feed.getDescription());
assertEquals("http://example.com/payment", feed.getPaymentLink());
assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url);
assertEquals("http://example.com/picture", feed.getImageUrl());
assertEquals(10, feed.getItems().size());
for (int i = 0; i < feed.getItems().size(); i++) {
@ -62,7 +62,7 @@ public class AtomParserTest {
assertEquals("http://example.com/feed", feed.getFeedIdentifier());
assertEquals("http://example.com", feed.getLink());
assertEquals("This is the description", feed.getDescription());
assertEquals("http://example.com/payment", feed.getPaymentLink());
assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url);
assertEquals("https://example.com/image.png", feed.getImageUrl());
assertEquals(0, feed.getItems().size());
}

View File

@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.syndication.handler;
import android.text.TextUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@ -31,7 +32,7 @@ public class RssParserTest {
assertEquals("en", feed.getLanguage());
assertEquals("http://example.com", feed.getLink());
assertEquals("This is the description", feed.getDescription());
assertEquals("http://example.com/payment", feed.getPaymentLink());
assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url);
assertEquals("http://example.com/picture", feed.getImageUrl());
assertEquals(10, feed.getItems().size());
for (int i = 0; i < feed.getItems().size(); i++) {
@ -62,7 +63,7 @@ public class RssParserTest {
assertEquals("title", feed.getTitle());
assertEquals("http://example.com", feed.getLink());
assertEquals("This is the description", feed.getDescription());
assertEquals("http://example.com/payment", feed.getPaymentLink());
assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url);
assertEquals("https://example.com/image.png", feed.getImageUrl());
assertEquals(0, feed.getItems().size());
}
@ -74,7 +75,7 @@ public class RssParserTest {
assertEquals("title", feed.getTitle());
assertEquals("http://example.com", feed.getLink());
assertEquals("This is the description", feed.getDescription());
assertEquals("http://example.com/payment", feed.getPaymentLink());
assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url);
assertNull(feed.getImageUrl());
assertEquals(1, feed.getItems().size());
FeedItem feedItem = feed.getItems().get(0);
@ -82,4 +83,17 @@ public class RssParserTest {
assertEquals(MediaType.VIDEO, feedItem.getMedia().getMediaType());
assertEquals("https://www.example.com/file.mp4", feedItem.getMedia().getDownload_url());
}
@Test
public void testMultipleFundingTags() throws Exception {
File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testMultipleFundingTags.xml");
Feed feed = FeedParserTestHelper.runFeedParser(feedFile);
assertEquals(3, feed.getPaymentLinks().size());
assertEquals("Text 1", feed.getPaymentLinks().get(0).content);
assertEquals("https://example.com/funding1", feed.getPaymentLinks().get(0).url);
assertEquals("Text 2", feed.getPaymentLinks().get(1).content);
assertEquals("https://example.com/funding2", feed.getPaymentLinks().get(1).url);
assertTrue(TextUtils.isEmpty(feed.getPaymentLinks().get(2).content));
assertEquals("https://example.com/funding3", feed.getPaymentLinks().get(2).url);
}
}

View File

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8' ?>
<rss version="2.0" xmlns:podcast="https://podcastindex.org/namespace/1.0">
<channel>
<title>title</title>
</channel>
<podcast:funding url="https://example.com/funding1">Text 1</podcast:funding>
<podcast:funding url="https://example.com/funding2">Text 2</podcast:funding>
<podcast:funding url="https://example.com/funding3" />
</rss>