From 24389d42e89037b205fff2bc681e4ad998895286 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sat, 28 Aug 2021 00:12:48 +0200 Subject: Moved feed parser to its own module --- app/build.gradle | 1 + .../syndication/feedgenerator/Rss2Generator.java | 6 +- .../activity/OnlineFeedViewActivity.java | 6 +- .../adapter/FeedItemlistDescriptionAdapter.java | 4 +- .../adapter/PlaybackStatisticsListAdapter.java | 4 +- .../danoeh/antennapod/fragment/CoverFragment.java | 4 +- .../danoeh/antennapod/fragment/ItemFragment.java | 6 +- .../view/viewholder/EpisodeItemViewHolder.java | 6 +- core/build.gradle | 1 + .../antennapod/core/export/opml/OpmlWriter.java | 4 +- .../antennapod/core/feed/LocalFeedUpdater.java | 2 +- .../danoeh/antennapod/core/feed/SimpleChapter.java | 16 -- .../core/service/download/HttpDownloader.java | 2 +- .../service/download/handler/FeedParserTask.java | 6 +- .../service/download/handler/FeedSyncTask.java | 2 +- .../core/storage/mapper/ChapterCursorMapper.java | 2 +- .../core/syndication/handler/FeedHandler.java | 90 -------- .../syndication/handler/FeedHandlerResult.java | 19 -- .../core/syndication/handler/HandlerState.java | 121 ----------- .../core/syndication/handler/SyndHandler.java | 138 ------------- .../core/syndication/handler/TypeGetter.java | 120 ----------- .../handler/UnsupportedFeedtypeException.java | 44 ---- .../core/syndication/namespace/NSContent.java | 25 --- .../core/syndication/namespace/NSDublinCore.java | 37 ---- .../core/syndication/namespace/NSITunes.java | 113 ----------- .../core/syndication/namespace/NSMedia.java | 132 ------------ .../core/syndication/namespace/NSRSS20.java | 145 ------------- .../syndication/namespace/NSSimpleChapters.java | 53 ----- .../core/syndication/namespace/Namespace.java | 21 -- .../core/syndication/namespace/PodcastIndex.java | 38 ---- .../core/syndication/namespace/SyndElement.java | 22 -- .../core/syndication/namespace/atom/AtomText.java | 38 ---- .../core/syndication/namespace/atom/NSAtom.java | 226 --------------------- .../core/syndication/parsers/DurationParser.java | 37 ---- .../core/syndication/util/SyndStringUtils.java | 14 -- .../core/syndication/util/SyndTypeUtils.java | 44 ---- .../danoeh/antennapod/core/util/DateFormatter.java | 46 +++++ .../de/danoeh/antennapod/core/util/DateUtils.java | 202 ------------------ .../antennapod/core/storage/DbTestUtils.java | 2 +- .../core/syndication/handler/AtomParserTest.java | 98 --------- .../syndication/handler/FeedParserTestHelper.java | 35 ---- .../core/syndication/handler/RssParserTest.java | 99 --------- .../syndication/namespace/atom/AtomTextTest.java | 35 ---- .../syndication/parsers/DurationParserTest.java | 43 ---- .../danoeh/antennapod/core/util/DateUtilsTest.java | 174 ---------------- .../src/test/resources/feed-atom-testAtomBasic.xml | 1 - .../test/resources/feed-atom-testEmptyRelLinks.xml | 14 -- .../resources/feed-atom-testLogoWithWhitespace.xml | 2 - .../resources/feed-rss-testImageWithWhitespace.xml | 2 - .../resources/feed-rss-testMediaContentMime.xml | 1 - .../resources/feed-rss-testMultipleFundingTags.xml | 9 - core/src/test/resources/feed-rss-testRss2Basic.xml | 1 - parser/README.md | 3 + parser/feed/README.md | 3 + parser/feed/build.gradle | 23 +++ parser/feed/src/main/AndroidManifest.xml | 1 + .../danoeh/antennapod/parser/feed/FeedHandler.java | 91 +++++++++ .../antennapod/parser/feed/FeedHandlerResult.java | 19 ++ .../antennapod/parser/feed/HandlerState.java | 120 +++++++++++ .../danoeh/antennapod/parser/feed/SyndHandler.java | 139 +++++++++++++ .../parser/feed/UnsupportedFeedtypeException.java | 45 ++++ .../antennapod/parser/feed/element/AtomText.java | 36 ++++ .../parser/feed/element/SimpleChapter.java | 16 ++ .../parser/feed/element/SyndElement.java | 22 ++ .../antennapod/parser/feed/namespace/Atom.java | 224 ++++++++++++++++++++ .../antennapod/parser/feed/namespace/Content.java | 24 +++ .../parser/feed/namespace/DublinCore.java | 38 ++++ .../antennapod/parser/feed/namespace/Itunes.java | 114 +++++++++++ .../antennapod/parser/feed/namespace/Media.java | 133 ++++++++++++ .../parser/feed/namespace/Namespace.java | 19 ++ .../parser/feed/namespace/PodcastIndex.java | 39 ++++ .../antennapod/parser/feed/namespace/Rss20.java | 148 ++++++++++++++ .../parser/feed/namespace/SimpleChapters.java | 54 +++++ .../antennapod/parser/feed/util/DateUtils.java | 163 +++++++++++++++ .../parser/feed/util/DurationParser.java | 37 ++++ .../parser/feed/util/SyndStringUtils.java | 14 ++ .../antennapod/parser/feed/util/SyndTypeUtils.java | 44 ++++ .../antennapod/parser/feed/util/TypeGetter.java | 121 +++++++++++ .../parser/feed/element/element/AtomTextTest.java | 37 ++++ .../feed/element/namespace/AtomParserTest.java | 98 +++++++++ .../element/namespace/FeedParserTestHelper.java | 36 ++++ .../feed/element/namespace/RssParserTest.java | 99 +++++++++ .../parser/feed/element/util/DateUtilsTest.java | 175 ++++++++++++++++ .../feed/element/util/DurationParserTest.java | 44 ++++ .../src/test/resources/feed-atom-testAtomBasic.xml | 1 + .../test/resources/feed-atom-testEmptyRelLinks.xml | 14 ++ .../resources/feed-atom-testLogoWithWhitespace.xml | 2 + .../resources/feed-rss-testImageWithWhitespace.xml | 2 + .../resources/feed-rss-testMediaContentMime.xml | 1 + .../resources/feed-rss-testMultipleFundingTags.xml | 9 + .../src/test/resources/feed-rss-testRss2Basic.xml | 1 + settings.gradle | 2 + 92 files changed, 2287 insertions(+), 2237 deletions(-) delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/PodcastIndex.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/parsers/DurationParser.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndStringUtils.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomTextTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/syndication/parsers/DurationParserTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java delete mode 100644 core/src/test/resources/feed-atom-testAtomBasic.xml delete mode 100644 core/src/test/resources/feed-atom-testEmptyRelLinks.xml delete mode 100644 core/src/test/resources/feed-atom-testLogoWithWhitespace.xml delete mode 100644 core/src/test/resources/feed-rss-testImageWithWhitespace.xml delete mode 100644 core/src/test/resources/feed-rss-testMediaContentMime.xml delete mode 100644 core/src/test/resources/feed-rss-testMultipleFundingTags.xml delete mode 100644 core/src/test/resources/feed-rss-testRss2Basic.xml create mode 100644 parser/README.md create mode 100644 parser/feed/README.md create mode 100644 parser/feed/build.gradle create mode 100644 parser/feed/src/main/AndroidManifest.xml create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/SyndHandler.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/UnsupportedFeedtypeException.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/AtomText.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SyndElement.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Content.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DateUtils.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DurationParser.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndStringUtils.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java create mode 100644 parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/element/AtomTextTest.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/AtomParserTest.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/FeedParserTestHelper.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DateUtilsTest.java create mode 100644 parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DurationParserTest.java create mode 100644 parser/feed/src/test/resources/feed-atom-testAtomBasic.xml create mode 100644 parser/feed/src/test/resources/feed-atom-testEmptyRelLinks.xml create mode 100644 parser/feed/src/test/resources/feed-atom-testLogoWithWhitespace.xml create mode 100644 parser/feed/src/test/resources/feed-rss-testImageWithWhitespace.xml create mode 100644 parser/feed/src/test/resources/feed-rss-testMediaContentMime.xml create mode 100644 parser/feed/src/test/resources/feed-rss-testMultipleFundingTags.xml create mode 100644 parser/feed/src/test/resources/feed-rss-testRss2Basic.xml diff --git a/app/build.gradle b/app/build.gradle index b2d6a5600..0d710c4cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -114,6 +114,7 @@ dependencies { implementation project(':model') implementation project(':net:sync:gpoddernet') implementation project(':net:sync:model') + implementation project(':parser:feed') implementation project(':ui:app-start-intent') implementation project(':ui:common') diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java index 9361c23df..6b294244a 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java @@ -11,8 +11,8 @@ import java.util.ArrayList; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedFunding; import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.syndication.namespace.PodcastIndex; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.parser.feed.namespace.PodcastIndex; +import de.danoeh.antennapod.core.util.DateFormatter; /** * Creates RSS 2.0 feeds. See FeedGenerator for more information. @@ -98,7 +98,7 @@ public class Rss2Generator implements FeedGenerator { } if (item.getPubDate() != null) { xml.startTag(null, "pubDate"); - xml.text(DateUtils.formatRFC822Date(item.getPubDate())); + xml.text(DateFormatter.formatRfc822Date(item.getPubDate())); xml.endTag(null, "pubDate"); } if ((flags & FEATURE_WRITE_GUID) != 0) { diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 575e94f8c..ec9e20dea 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -46,9 +46,8 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.parser.feed.FeedHandler; +import de.danoeh.antennapod.parser.feed.FeedHandlerResult; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.IntentUtils; @@ -63,6 +62,7 @@ import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.playback.RemoteMedia; +import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index 62a97e849..2ab96e84d 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -17,7 +17,7 @@ import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.model.playback.RemoteMedia; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; @@ -58,7 +58,7 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter { } holder.title.setText(item.getTitle()); - holder.pubDate.setText(DateUtils.formatAbbrev(getContext(), item.getPubDate())); + holder.pubDate.setText(DateFormatter.formatAbbrev(getContext(), item.getPubDate())); if (item.getDescription() != null) { String description = HtmlToPlainText.getPlainText(item.getDescription()) .replaceAll("\n", " ") diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java index a71595c55..5fec5f063 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/PlaybackStatisticsListAdapter.java @@ -7,7 +7,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.StatisticsItem; import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.view.PieChartView; import java.util.Date; @@ -32,7 +32,7 @@ public class PlaybackStatisticsListAdapter extends StatisticsListAdapter { String getHeaderCaption() { long usageCounting = UserPreferences.getUsageCountingDateMillis(); if (usageCounting > 0) { - String date = DateUtils.formatAbbrev(context, new Date(usageCounting)); + String date = DateFormatter.formatAbbrev(context, new Date(usageCounting)); return context.getString(R.string.statistics_counting_since, date); } else { return context.getString(R.string.total_time_listened_to_podcasts); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 5dbadaefa..67d1757ac 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -50,7 +50,7 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.model.feed.Chapter; @@ -150,7 +150,7 @@ public class CoverFragment extends Fragment { } private void displayMediaInfo(@NonNull Playable media) { - String pubDateStr = DateUtils.formatAbbrev(getActivity(), media.getPubDate()); + String pubDateStr = DateFormatter.formatAbbrev(getActivity(), media.getPubDate()); txtvPodcastTitle.setText(StringUtils.stripToEmpty(media.getFeedTitle()) + "\u00A0" + "・" diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index 5a2061a5f..31c6da8cd 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -55,7 +55,7 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.ui.common.ThemeUtils; import de.danoeh.antennapod.core.util.playback.PlaybackController; @@ -291,9 +291,9 @@ public class ItemFragment extends Fragment { txtvTitle.setText(item.getTitle()); if (item.getPubDate() != null) { - String pubDateStr = DateUtils.formatAbbrev(getActivity(), item.getPubDate()); + String pubDateStr = DateFormatter.formatAbbrev(getActivity(), item.getPubDate()); txtvPublished.setText(pubDateStr); - txtvPublished.setContentDescription(DateUtils.formatForAccessibility(getContext(), item.getPubDate())); + txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(getContext(), item.getPubDate())); } RequestOptions options = new RequestOptions() diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index 02d45b2a0..cd3af5003 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -22,6 +22,7 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.CoverLoader; import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.util.DateFormatter; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.playback.MediaType; @@ -31,7 +32,6 @@ import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.ui.common.CircularProgressBar; @@ -103,8 +103,8 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { placeholder.setText(item.getFeed().getTitle()); title.setText(item.getTitle()); leftPadding.setContentDescription(item.getTitle()); - pubDate.setText(DateUtils.formatAbbrev(activity, item.getPubDate())); - pubDate.setContentDescription(DateUtils.formatForAccessibility(activity, item.getPubDate())); + pubDate.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); + pubDate.setContentDescription(DateFormatter.formatForAccessibility(activity, item.getPubDate())); isNew.setVisibility(item.isNew() ? View.VISIBLE : View.GONE); isFavorite.setVisibility(item.isTagged(FeedItem.TAG_FAVORITE) ? View.VISIBLE : View.GONE); isInQueue.setVisibility(item.isTagged(FeedItem.TAG_QUEUE) ? View.VISIBLE : View.GONE); diff --git a/core/build.gradle b/core/build.gradle index e78b70881..2190ad35f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation project(':net:ssl') implementation project(':net:sync:gpoddernet') implementation project(':net:sync:model') + implementation project(':parser:feed') implementation project(':ui:app-start-intent') implementation project(':ui:common') implementation project(':ui:png-icons') diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java index e2205471c..a44d90557 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java @@ -4,6 +4,7 @@ import android.content.Context; import android.util.Log; import android.util.Xml; +import de.danoeh.antennapod.core.util.DateFormatter; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; @@ -13,7 +14,6 @@ import java.util.List; import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.util.DateUtils; /** Writes OPML documents. */ public class OpmlWriter implements ExportWriter { @@ -44,7 +44,7 @@ public class OpmlWriter implements ExportWriter { xs.text(OPML_TITLE); xs.endTag(null, OpmlSymbols.TITLE); xs.startTag(null, OpmlSymbols.DATE_CREATED); - xs.text(DateUtils.formatRFC822Date(new Date())); + xs.text(DateFormatter.formatRfc822Date(new Date())); xs.endTag(null, OpmlSymbols.DATE_CREATED); xs.endTag(null, OpmlSymbols.HEAD); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 7a8c4969b..82583b7b5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -27,7 +27,7 @@ import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.parser.feed.util.DateUtils; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java deleted file mode 100644 index ca59f867b..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import de.danoeh.antennapod.model.feed.Chapter; - -public class SimpleChapter extends Chapter { - public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; - - public SimpleChapter(long start, String title, String link, String imageUrl) { - super(start, title, link, imageUrl); - } - - @Override - public int getChapterType() { - return CHAPTERTYPE_SIMPLECHAPTER; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 1320b7052..781110f82 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -24,7 +24,7 @@ import java.util.regex.Pattern; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.parser.feed.util.DateUtils; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URIUtil; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index 3e3da00e7..9a0916109 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -8,9 +8,9 @@ import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.syndication.handler.FeedHandler; -import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; -import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; +import de.danoeh.antennapod.parser.feed.FeedHandler; +import de.danoeh.antennapod.parser.feed.FeedHandlerResult; +import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.InvalidFeedException; import org.xml.sax.SAXException; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 1ca4d1194..dcc1c8fdb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -9,7 +9,7 @@ import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; +import de.danoeh.antennapod.parser.feed.FeedHandlerResult; public class FeedSyncTask { private static final String TAG = "FeedParserTask"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java index b171f2bcc..61613a25a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/ChapterCursorMapper.java @@ -4,7 +4,7 @@ import android.database.Cursor; import androidx.annotation.NonNull; import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.core.feed.ID3Chapter; -import de.danoeh.antennapod.core.feed.SimpleChapter; +import de.danoeh.antennapod.parser.feed.element.SimpleChapter; import de.danoeh.antennapod.core.feed.VorbisCommentChapter; import de.danoeh.antennapod.core.storage.PodDBAdapter; diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java deleted file mode 100644 index 2928ba836..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java +++ /dev/null @@ -1,90 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import android.text.TextUtils; -import android.util.Log; - -import org.apache.commons.io.input.XmlStreamReader; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; - -public class FeedHandler { - private static final String TAG = "FeedHandler"; - - public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, - ParserConfigurationException, UnsupportedFeedtypeException { - TypeGetter tg = new TypeGetter(); - TypeGetter.Type type = tg.getType(feed); - SyndHandler handler = new SyndHandler(feed, type); - - SAXParserFactory factory = SAXParserFactory.newInstance(); - factory.setNamespaceAware(true); - SAXParser saxParser = factory.newSAXParser(); - File file = new File(feed.getFile_url()); - Reader inputStreamReader = new XmlStreamReader(file); - InputSource inputSource = new InputSource(inputStreamReader); - - saxParser.parse(inputSource, handler); - inputStreamReader.close(); - feed.setItems(dedupItems(feed.getItems())); - return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); - } - - /** - * For updating items that are stored in the database, see also: DBTasks.searchFeedItemByIdentifyingValue - */ - public static List dedupItems(List items) { - if (items == null) { - return null; - } - List list = new ArrayList<>(items); - Set seen = new HashSet<>(); - Iterator it = list.iterator(); - while (it.hasNext()) { - FeedItem item = it.next(); - if (!TextUtils.isEmpty(item.getItemIdentifier()) && seen.contains(item.getItemIdentifier())) { - Log.d(TAG, "Removing duplicate episode guid " + item.getItemIdentifier()); - it.remove(); - continue; - } - - if (item.getMedia() == null || TextUtils.isEmpty(item.getMedia().getStreamUrl())) { - continue; - } - if (seen.contains(item.getMedia().getStreamUrl())) { - Log.d(TAG, "Removing duplicate episode stream url " + item.getMedia().getStreamUrl()); - it.remove(); - } else { - seen.add(item.getMedia().getStreamUrl()); - if (TextUtils.isEmpty(item.getTitle()) || item.getPubDate() == null) { - continue; - } - if (!seen.contains(item.getTitle() + item.getPubDate().toString())) { - seen.add(item.getTitle() + item.getPubDate().toString()); - } else { - Log.d(TAG, "Removing duplicate episode title and pubDate " - + item.getTitle() - + " " + item.getPubDate()); - it.remove(); - } - } - seen.add(item.getItemIdentifier()); - } - return list; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java deleted file mode 100644 index fb4bf4707..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import java.util.Map; - -import de.danoeh.antennapod.model.feed.Feed; - -/** - * Container for results returned by the Feed parser - */ -public class FeedHandlerResult { - - public final Feed feed; - public final Map alternateFeedUrls; - - public FeedHandlerResult(Feed feed, Map alternateFeedUrls) { - this.feed = feed; - this.alternateFeedUrls = alternateFeedUrls; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java deleted file mode 100644 index 2fecb0536..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java +++ /dev/null @@ -1,121 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import androidx.collection.ArrayMap; - -import java.util.ArrayList; -import java.util.Map; -import java.util.Stack; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedFunding; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.syndication.namespace.Namespace; -import de.danoeh.antennapod.core.syndication.namespace.SyndElement; - -/** - * Contains all relevant information to describe the current state of a - * SyndHandler. - */ -public class HandlerState { - - /** - * Feed that the Handler is currently processing. - */ - Feed feed; - /** - * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the - * URL of the feed, the value is the title - */ - final Map alternateUrls; - private final ArrayList items; - private FeedItem currentItem; - private FeedFunding currentFunding; - final Stack tagstack; - /** - * Namespaces that have been defined so far. - */ - final Map namespaces; - final Stack defaultNamespaces; - /** - * Buffer for saving characters. - */ - protected StringBuilder contentBuf; - - /** - * Temporarily saved objects. - */ - private final Map tempObjects; - - public HandlerState(Feed feed) { - this.feed = feed; - alternateUrls = new ArrayMap<>(); - items = new ArrayList<>(); - tagstack = new Stack<>(); - namespaces = new ArrayMap<>(); - defaultNamespaces = new Stack<>(); - tempObjects = new ArrayMap<>(); - } - - public Feed getFeed() { - return feed; - } - - public ArrayList getItems() { - return items; - } - - public FeedItem getCurrentItem() { - return currentItem; - } - - public Stack getTagstack() { - return tagstack; - } - - public void setFeed(Feed feed) { - this.feed = feed; - } - - public void setCurrentItem(FeedItem currentItem) { - 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. - */ - public SyndElement getSecondTag() { - SyndElement top = tagstack.pop(); - SyndElement second = tagstack.peek(); - tagstack.push(top); - return second; - } - - public SyndElement getThirdTag() { - SyndElement top = tagstack.pop(); - SyndElement second = tagstack.pop(); - SyndElement third = tagstack.peek(); - tagstack.push(second); - tagstack.push(top); - return third; - } - - public StringBuilder getContentBuf() { - return contentBuf; - } - - public void addAlternateFeedUrl(String title, String url) { - alternateUrls.put(url, title); - } - - public Map getTempObjects() { - return tempObjects; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java deleted file mode 100644 index 9c09be714..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import android.util.Log; - -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.syndication.namespace.NSContent; -import de.danoeh.antennapod.core.syndication.namespace.NSDublinCore; -import de.danoeh.antennapod.core.syndication.namespace.NSITunes; -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; - -/** Superclass for all SAX Handlers which process Syndication formats */ -class SyndHandler extends DefaultHandler { - private static final String TAG = "SyndHandler"; - private static final String DEFAULT_PREFIX = ""; - final HandlerState state; - - public SyndHandler(Feed feed, TypeGetter.Type type) { - state = new HandlerState(feed); - if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { - state.defaultNamespaces.push(new NSRSS20()); - } - } - - @Override - public void startElement(String uri, String localName, String qName, - Attributes attributes) throws SAXException { - state.contentBuf = new StringBuilder(); - Namespace handler = getHandlingNamespace(uri, qName); - if (handler != null) { - SyndElement element = handler.handleElementStart(localName, state, - attributes); - state.tagstack.push(element); - - } - } - - @Override - public void characters(char[] ch, int start, int length) - throws SAXException { - if (!state.tagstack.empty()) { - if (state.getTagstack().size() >= 2) { - if (state.contentBuf != null) { - state.contentBuf.append(ch, start, length); - } - } - } - } - - @Override - public void endElement(String uri, String localName, String qName) - throws SAXException { - Namespace handler = getHandlingNamespace(uri, qName); - if (handler != null) { - handler.handleElementEnd(localName, state); - state.tagstack.pop(); - - } - state.contentBuf = null; - - } - - @Override - public void endPrefixMapping(String prefix) throws SAXException { - if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { - state.defaultNamespaces.pop(); - } - } - - @Override - public void startPrefixMapping(String prefix, String uri) - throws SAXException { - // Find the right namespace - if (!state.namespaces.containsKey(uri)) { - if (uri.equals(NSAtom.NSURI)) { - if (prefix.equals(DEFAULT_PREFIX)) { - state.defaultNamespaces.push(new NSAtom()); - } else if (prefix.equals(NSAtom.NSTAG)) { - state.namespaces.put(uri, new NSAtom()); - Log.d(TAG, "Recognized Atom namespace"); - } - } else if (uri.equals(NSContent.NSURI) - && prefix.equals(NSContent.NSTAG)) { - state.namespaces.put(uri, new NSContent()); - Log.d(TAG, "Recognized Content namespace"); - } else if (uri.equals(NSITunes.NSURI) - && prefix.equals(NSITunes.NSTAG)) { - state.namespaces.put(uri, new NSITunes()); - Log.d(TAG, "Recognized ITunes namespace"); - } else if (uri.equals(NSSimpleChapters.NSURI) - && prefix.matches(NSSimpleChapters.NSTAG)) { - state.namespaces.put(uri, new NSSimpleChapters()); - Log.d(TAG, "Recognized SimpleChapters namespace"); - } else if (uri.equals(NSMedia.NSURI) - && prefix.equals(NSMedia.NSTAG)) { - state.namespaces.put(uri, new NSMedia()); - Log.d(TAG, "Recognized media namespace"); - } else if (uri.equals(NSDublinCore.NSURI) - && 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); - if (handler == null && !state.defaultNamespaces.empty() - && !qName.contains(":")) { - handler = state.defaultNamespaces.peek(); - } - return handler; - } - - @Override - public void endDocument() throws SAXException { - super.endDocument(); - state.getFeed().setItems(state.getItems()); - } - - public HandlerState getState() { - return state; - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java deleted file mode 100644 index e6011e3fa..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import android.util.Log; - -import org.apache.commons.io.input.XmlStreamReader; -import org.jsoup.Jsoup; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Reader; - -import de.danoeh.antennapod.model.feed.Feed; - -/** Gets the type of a specific feed by reading the root element. */ -public class TypeGetter { - private static final String TAG = "TypeGetter"; - - public enum Type { - RSS20, RSS091, ATOM, INVALID - } - - private static final String ATOM_ROOT = "feed"; - private static final String RSS_ROOT = "rss"; - - public Type getType(Feed feed) throws UnsupportedFeedtypeException { - XmlPullParserFactory factory; - if (feed.getFile_url() != null) { - Reader reader = null; - try { - factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - XmlPullParser xpp = factory.newPullParser(); - reader = createReader(feed); - xpp.setInput(reader); - int eventType = xpp.getEventType(); - - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG) { - String tag = xpp.getName(); - switch (tag) { - case ATOM_ROOT: - feed.setType(Feed.TYPE_ATOM1); - Log.d(TAG, "Recognized type Atom"); - - String strLang = xpp.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang"); - if (strLang != null) { - feed.setLanguage(strLang); - } - - return Type.ATOM; - case RSS_ROOT: - String strVersion = xpp.getAttributeValue(null, "version"); - if (strVersion == null) { - feed.setType(Feed.TYPE_RSS2); - Log.d(TAG, "Assuming type RSS 2.0"); - return Type.RSS20; - } else if (strVersion.equals("2.0")) { - feed.setType(Feed.TYPE_RSS2); - Log.d(TAG, "Recognized type RSS 2.0"); - return Type.RSS20; - } else if (strVersion.equals("0.91") || strVersion.equals("0.92")) { - Log.d(TAG, "Recognized type RSS 0.91/0.92"); - return Type.RSS091; - } - throw new UnsupportedFeedtypeException("Unsupported rss version"); - default: - Log.d(TAG, "Type is invalid"); - throw new UnsupportedFeedtypeException(Type.INVALID, tag); - } - } else { - eventType = xpp.next(); - } - } - } catch (XmlPullParserException e) { - e.printStackTrace(); - // XML document might actually be a HTML document -> try to parse as HTML - String rootElement = null; - try { - if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { - rootElement = "html"; - } - } catch (IOException e1) { - e1.printStackTrace(); - } - throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); - - } catch (IOException e) { - e.printStackTrace(); - } finally { - if(reader != null) { - try { - reader.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - Log.d(TAG, "Type is invalid"); - throw new UnsupportedFeedtypeException(Type.INVALID); - } - - private Reader createReader(Feed feed) { - Reader reader; - try { - reader = new XmlStreamReader(new File(feed.getFile_url())); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } catch (IOException e) { - e.printStackTrace(); - return null; - } - return reader; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java deleted file mode 100644 index c9f9f19c8..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type; - -public class UnsupportedFeedtypeException extends Exception { - private static final long serialVersionUID = 9105878964928170669L; - private final TypeGetter.Type type; - private String rootElement; - private String message = null; - - public UnsupportedFeedtypeException(Type type) { - super(); - this.type = type; - } - - public UnsupportedFeedtypeException(Type type, String rootElement) { - this.type = type; - this.rootElement = rootElement; - } - - public UnsupportedFeedtypeException(String message) { - this.message = message; - type = Type.INVALID; - } - - public TypeGetter.Type getType() { - return type; - } - - public String getRootElement() { - return rootElement; - } - - @Override - public String getMessage() { - if (message != null) { - return message; - } else if (type == TypeGetter.Type.INVALID) { - return "Invalid type"; - } else { - return "Type " + type + " not supported"; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java deleted file mode 100644 index bedf377aa..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.core.syndication.handler.HandlerState; - -public class NSContent extends Namespace { - public static final String NSTAG = "content"; - public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; - - private static final String ENCODED = "encoded"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (ENCODED.equals(localName) && state.getCurrentItem() != null && state.getContentBuf() != null) { - state.getCurrentItem().setDescriptionIfLonger(state.getContentBuf().toString()); - } - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java deleted file mode 100644 index 0394b754a..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.util.DateUtils; - -public class NSDublinCore extends Namespace { - private static final String TAG = "NSDublinCore"; - public static final String NSTAG = "dc"; - public static final String NSURI = "http://purl.org/dc/elements/1.1/"; - - private static final String ITEM = "item"; - private static final String DATE = "date"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (state.getCurrentItem() != null && state.getContentBuf() != null && - state.getTagstack() != null && state.getTagstack().size() >= 2) { - FeedItem currentItem = state.getCurrentItem(); - String top = state.getTagstack().peek().getName(); - String second = state.getSecondTag().getName(); - if (DATE.equals(top) && ITEM.equals(second)) { - String content = state.getContentBuf().toString(); - currentItem.setPubDate(DateUtils.parseOrNullIfFuture(content)); - } - } - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java deleted file mode 100644 index 1dc8d8af3..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java +++ /dev/null @@ -1,113 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import android.text.TextUtils; -import android.util.Log; - -import androidx.core.text.HtmlCompat; - -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.parsers.DurationParser; - -public class NSITunes extends Namespace { - - public static final String NSTAG = "itunes"; - public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; - - private static final String IMAGE = "image"; - private static final String IMAGE_HREF = "href"; - - private static final String AUTHOR = "author"; - public static final String DURATION = "duration"; - private static final String SUBTITLE = "subtitle"; - private static final String SUMMARY = "summary"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (IMAGE.equals(localName)) { - String url = attributes.getValue(IMAGE_HREF); - - if (state.getCurrentItem() != null) { - state.getCurrentItem().setImageUrl(url); - } else { - // this is the feed image - // prefer to all other images - if (!TextUtils.isEmpty(url)) { - state.getFeed().setImageUrl(url); - } - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (state.getContentBuf() == null) { - return; - } - - if (AUTHOR.equals(localName)) { - parseAuthor(state); - } else if (DURATION.equals(localName)) { - parseDuration(state); - } else if (SUBTITLE.equals(localName)) { - parseSubtitle(state); - } else if (SUMMARY.equals(localName)) { - SyndElement secondElement = state.getSecondTag(); - parseSummary(state, secondElement.getName()); - } - } - - private void parseAuthor(HandlerState state) { - if (state.getFeed() != null) { - String author = state.getContentBuf().toString(); - state.getFeed().setAuthor(HtmlCompat.fromHtml(author, - HtmlCompat.FROM_HTML_MODE_LEGACY).toString()); - } - } - - private void parseDuration(HandlerState state) { - String durationStr = state.getContentBuf().toString(); - if (TextUtils.isEmpty(durationStr)) { - return; - } - - try { - long durationMs = DurationParser.inMillis(durationStr); - state.getTempObjects().put(DURATION, (int) durationMs); - } catch (NumberFormatException e) { - Log.e(NSTAG, String.format("Duration '%s' could not be parsed", durationStr)); - } - } - - private void parseSubtitle(HandlerState state) { - String subtitle = state.getContentBuf().toString(); - if (TextUtils.isEmpty(subtitle)) { - return; - } - if (state.getCurrentItem() != null) { - if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) { - state.getCurrentItem().setDescriptionIfLonger(subtitle); - } - } else { - if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) { - state.getFeed().setDescription(subtitle); - } - } - } - - private void parseSummary(HandlerState state, String secondElementName) { - String summary = state.getContentBuf().toString(); - if (TextUtils.isEmpty(summary)) { - return; - } - - if (state.getCurrentItem() != null) { - state.getCurrentItem().setDescriptionIfLonger(summary); - } else if (NSRSS20.CHANNEL.equals(secondElementName) && state.getFeed() != null) { - state.getFeed().setDescription(summary); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java deleted file mode 100644 index 3dba0735d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java +++ /dev/null @@ -1,132 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import android.text.TextUtils; -import android.util.Log; - -import org.xml.sax.Attributes; - -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.namespace.atom.AtomText; -import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; - -/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ -public class NSMedia extends Namespace { - private static final String TAG = "NSMedia"; - - public static final String NSTAG = "media"; - public static final String NSURI = "http://search.yahoo.com/mrss/"; - - private static final String CONTENT = "content"; - private static final String DOWNLOAD_URL = "url"; - private static final String SIZE = "fileSize"; - private static final String MIME_TYPE = "type"; - private static final String DURATION = "duration"; - private static final String DEFAULT = "isDefault"; - private static final String MEDIUM = "medium"; - - private static final String MEDIUM_IMAGE = "image"; - private static final String MEDIUM_AUDIO = "audio"; - private static final String MEDIUM_VIDEO = "video"; - - private static final String IMAGE = "thumbnail"; - private static final String IMAGE_URL = "url"; - - private static final String DESCRIPTION = "description"; - private static final String DESCRIPTION_TYPE = "type"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (CONTENT.equals(localName)) { - String url = attributes.getValue(DOWNLOAD_URL); - String type = attributes.getValue(MIME_TYPE); - String defaultStr = attributes.getValue(DEFAULT); - String medium = attributes.getValue(MEDIUM); - boolean validTypeMedia = false; - boolean validTypeImage = false; - boolean isDefault = "true".equals(defaultStr); - String guessedType = SyndTypeUtils.getMimeTypeFromUrl(url); - - if (MEDIUM_AUDIO.equals(medium)) { - validTypeMedia = true; - type = "audio/*"; - } else if (MEDIUM_VIDEO.equals(medium)) { - validTypeMedia = true; - type = "video/*"; - } else if (MEDIUM_IMAGE.equals(medium) && (guessedType == null - || (!guessedType.startsWith("audio/") && !guessedType.startsWith("video/")))) { - // Apparently, some publishers explicitly specify the audio file as an image - validTypeImage = true; - type = "image/*"; - } else { - if (type == null) { - type = guessedType; - } - - if (SyndTypeUtils.enclosureTypeValid(type)) { - validTypeMedia = true; - } else if (SyndTypeUtils.imageTypeValid(type)) { - validTypeImage = true; - } - } - - if (state.getCurrentItem() != null && (state.getCurrentItem().getMedia() == null || isDefault) - && url != null && validTypeMedia) { - long size = 0; - String sizeStr = attributes.getValue(SIZE); - try { - size = Long.parseLong(sizeStr); - } catch (NumberFormatException e) { - Log.e(TAG, "Size \"" + sizeStr + "\" could not be parsed."); - } - - int durationMs = 0; - String durationStr = attributes.getValue(DURATION); - if (!TextUtils.isEmpty(durationStr)) { - try { - long duration = Long.parseLong(durationStr); - durationMs = (int) TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS); - } catch (NumberFormatException e) { - Log.e(TAG, "Duration \"" + durationStr + "\" could not be parsed"); - } - } - FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type); - if (durationMs > 0) { - media.setDuration(durationMs); - } - state.getCurrentItem().setMedia(media); - } else if (state.getCurrentItem() != null && url != null && validTypeImage) { - state.getCurrentItem().setImageUrl(url); - } - } else if (IMAGE.equals(localName)) { - String url = attributes.getValue(IMAGE_URL); - if (url != null) { - if (state.getCurrentItem() != null) { - state.getCurrentItem().setImageUrl(url); - } else { - if (state.getFeed().getImageUrl() == null) { - state.getFeed().setImageUrl(url); - } - } - } - } else if (DESCRIPTION.equals(localName)) { - String type = attributes.getValue(DESCRIPTION_TYPE); - return new AtomText(localName, this, type); - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (DESCRIPTION.equals(localName)) { - String content = state.getContentBuf().toString(); - if (state.getCurrentItem() != null) { - state.getCurrentItem().setDescriptionIfLonger(content); - } - } - } -} - diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java deleted file mode 100644 index 50c2dc118..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ /dev/null @@ -1,145 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import android.text.TextUtils; -import android.util.Log; - -import de.danoeh.antennapod.core.syndication.util.SyndStringUtils; -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; -import de.danoeh.antennapod.core.util.DateUtils; - -/** - * SAX-Parser for reading RSS-Feeds. - */ -public class NSRSS20 extends Namespace { - - private static final String TAG = "NSRSS20"; - - public static final String CHANNEL = "channel"; - public static final String ITEM = "item"; - private static final String GUID = "guid"; - private static final String TITLE = "title"; - private static final String LINK = "link"; - private static final String DESCR = "description"; - private static final String PUBDATE = "pubDate"; - private static final String ENCLOSURE = "enclosure"; - private static final String IMAGE = "image"; - private static final String URL = "url"; - private static final String LANGUAGE = "language"; - - private static final String ENC_URL = "url"; - private static final String ENC_LEN = "length"; - private static final String ENC_TYPE = "type"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (ITEM.equals(localName)) { - state.setCurrentItem(new FeedItem()); - state.getItems().add(state.getCurrentItem()); - state.getCurrentItem().setFeed(state.getFeed()); - - } else if (ENCLOSURE.equals(localName)) { - String type = attributes.getValue(ENC_TYPE); - String url = attributes.getValue(ENC_URL); - - boolean validType = SyndTypeUtils.enclosureTypeValid(type); - if (!validType) { - type = SyndTypeUtils.getMimeTypeFromUrl(url); - validType = SyndTypeUtils.enclosureTypeValid(type); - } - - boolean validUrl = !TextUtils.isEmpty(url); - if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null - && validType && validUrl) { - long size = 0; - try { - size = Long.parseLong(attributes.getValue(ENC_LEN)); - if (size < 16384) { - // less than 16kb is suspicious, check manually - size = 0; - } - } catch (NumberFormatException e) { - Log.d(TAG, "Length attribute could not be parsed."); - } - FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type); - state.getCurrentItem().setMedia(media); - } - - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (ITEM.equals(localName)) { - if (state.getCurrentItem() != null) { - FeedItem currentItem = state.getCurrentItem(); - // the title tag is optional in RSS 2.0. The description is used - // as a title if the item has no title-tag. - if (currentItem.getTitle() == null) { - currentItem.setTitle(currentItem.getDescription()); - } - - if (state.getTempObjects().containsKey(NSITunes.DURATION)) { - if (currentItem.hasMedia()) { - Integer duration = (Integer) state.getTempObjects().get(NSITunes.DURATION); - currentItem.getMedia().setDuration(duration); - } - state.getTempObjects().remove(NSITunes.DURATION); - } - } - state.setCurrentItem(null); - } else if (state.getTagstack().size() >= 2 && state.getContentBuf() != null) { - String contentRaw = state.getContentBuf().toString(); - String content = SyndStringUtils.trimAllWhitespace(contentRaw); - SyndElement topElement = state.getTagstack().peek(); - String top = topElement.getName(); - SyndElement secondElement = state.getSecondTag(); - String second = secondElement.getName(); - String third = null; - if (state.getTagstack().size() >= 3) { - third = state.getThirdTag().getName(); - } - if (GUID.equals(top) && ITEM.equals(second)) { - // some feed creators include an empty or non-standard guid-element in their feed, - // which should be ignored - if (!TextUtils.isEmpty(contentRaw) && state.getCurrentItem() != null) { - state.getCurrentItem().setItemIdentifier(contentRaw); - } - } else if (TITLE.equals(top)) { - if (ITEM.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setTitle(content); - } else if (CHANNEL.equals(second) && state.getFeed() != null) { - state.getFeed().setTitle(content); - } - } else if (LINK.equals(top)) { - if (CHANNEL.equals(second) && state.getFeed() != null) { - state.getFeed().setLink(content); - } else if (ITEM.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setLink(content); - } - } else if (PUBDATE.equals(top) && ITEM.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); - } else if (URL.equals(top) && IMAGE.equals(second) && CHANNEL.equals(third)) { - // prefer itunes:image - if (state.getFeed() != null && state.getFeed().getImageUrl() == null) { - state.getFeed().setImageUrl(content); - } - } else if (DESCR.equals(localName)) { - if (CHANNEL.equals(second) && state.getFeed() != null) { - state.getFeed().setDescription(content); - } else if (ITEM.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setDescriptionIfLonger(content); - } - } else if (LANGUAGE.equals(localName) && state.getFeed() != null) { - state.getFeed().setLanguage(content.toLowerCase()); - } - } - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java deleted file mode 100644 index 97d0ebb53..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import android.util.Log; - -import org.xml.sax.Attributes; - -import java.util.ArrayList; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.feed.SimpleChapter; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.util.DateUtils; - -public class NSSimpleChapters extends Namespace { - private static final String TAG = "NSSimpleChapters"; - - public static final String NSTAG = "psc|sc"; - public static final String NSURI = "http://podlove.org/simple-chapters"; - - private static final String CHAPTERS = "chapters"; - private static final String CHAPTER = "chapter"; - private static final String START = "start"; - private static final String TITLE = "title"; - private static final String HREF = "href"; - private static final String IMAGE = "image"; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { - FeedItem currentItem = state.getCurrentItem(); - if (currentItem != null) { - if (localName.equals(CHAPTERS)) { - currentItem.setChapters(new ArrayList<>()); - } else if (localName.equals(CHAPTER)) { - try { - long start = DateUtils.parseTimeString(attributes.getValue(START)); - String title = attributes.getValue(TITLE); - String link = attributes.getValue(HREF); - String imageUrl = attributes.getValue(IMAGE); - SimpleChapter chapter = new SimpleChapter(start, title, link, imageUrl); - currentItem.getChapters().add(chapter); - } catch (NumberFormatException e) { - Log.e(TAG, "Unable to read chapter", e); - } - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java deleted file mode 100644 index e5fbdb9bb..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.core.syndication.handler.HandlerState; - - -public abstract class Namespace { - public static final String NSTAG = null; - public static final String NSURI = null; - - /** Called by a Feedhandler when in startElement and it detects a namespace element - * @return The SyndElement to push onto the stack - * */ - public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); - - /** Called by a Feedhandler when in endElement and it detects a namespace element - * */ - public abstract void handleElementEnd(String localName, HandlerState state); - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/PodcastIndex.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/PodcastIndex.java deleted file mode 100644 index ee150f839..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/PodcastIndex.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -import org.jsoup.helper.StringUtil; -import org.xml.sax.Attributes; -import de.danoeh.antennapod.model.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); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java deleted file mode 100644 index ba1b8ba5c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace; - -/** Defines a XML Element that is pushed on the tagstack */ -public class SyndElement { - private final String name; - private final Namespace namespace; - - public SyndElement(String name, Namespace namespace) { - this.name = name; - this.namespace = namespace; - } - - public Namespace getNamespace() { - return namespace; - } - - public String getName() { - return name; - } - - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java deleted file mode 100644 index 0c0561279..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace.atom; - -import androidx.core.text.HtmlCompat; - -import de.danoeh.antennapod.core.syndication.namespace.Namespace; -import de.danoeh.antennapod.core.syndication.namespace.SyndElement; - -/** Represents Atom Element which contains text (content, title, summary). */ -public class AtomText extends SyndElement { - public static final String TYPE_TEXT = "text"; - public static final String TYPE_HTML = "html"; - private static final String TYPE_XHTML = "xhtml"; - - private final String type; - private String content; - - public AtomText(String name, Namespace namespace, String type) { - super(name, namespace); - this.type = type; - } - - /** Processes the content according to the type and returns it. */ - public String getProcessedContent() { - if (type == null) { - return content; - } else if (type.equals(TYPE_HTML)) { - return HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_LEGACY).toString(); - } else if (type.equals(TYPE_XHTML)) { - return content; - } else { // Handle as text by default - return content; - } - } - - public void setContent(String content) { - this.content = content; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java deleted file mode 100644 index b93f41771..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ /dev/null @@ -1,226 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace.atom; - -import android.text.TextUtils; -import android.util.Log; - -import de.danoeh.antennapod.model.feed.FeedFunding; -import de.danoeh.antennapod.core.syndication.util.SyndStringUtils; -import org.xml.sax.Attributes; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; -import de.danoeh.antennapod.core.syndication.namespace.NSITunes; -import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; -import de.danoeh.antennapod.core.syndication.namespace.Namespace; -import de.danoeh.antennapod.core.syndication.namespace.SyndElement; -import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; -import de.danoeh.antennapod.core.util.DateUtils; - -public class NSAtom extends Namespace { - private static final String TAG = "NSAtom"; - public static final String NSTAG = "atom"; - public static final String NSURI = "http://www.w3.org/2005/Atom"; - - private static final String FEED = "feed"; - private static final String ID = "id"; - private static final String TITLE = "title"; - private static final String ENTRY = "entry"; - private static final String LINK = "link"; - private static final String UPDATED = "updated"; - private static final String AUTHOR = "author"; - private static final String AUTHOR_NAME = "name"; - private static final String CONTENT = "content"; - private static final String SUMMARY = "summary"; - private static final String IMAGE_LOGO = "logo"; - private static final String IMAGE_ICON = "icon"; - private static final String SUBTITLE = "subtitle"; - private static final String PUBLISHED = "published"; - - private static final String TEXT_TYPE = "type"; - // Link - private static final String LINK_HREF = "href"; - private static final String LINK_REL = "rel"; - private static final String LINK_TYPE = "type"; - private static final String LINK_TITLE = "title"; - private static final String LINK_LENGTH = "length"; - // rel-values - private static final String LINK_REL_ALTERNATE = "alternate"; - private static final String LINK_REL_ARCHIVES = "archives"; - private static final String LINK_REL_ENCLOSURE = "enclosure"; - private static final String LINK_REL_PAYMENT = "payment"; - private static final String LINK_REL_NEXT = "next"; - // type-values - private static final String LINK_TYPE_ATOM = "application/atom+xml"; - private static final String LINK_TYPE_HTML = "text/html"; - private static final String LINK_TYPE_XHTML = "application/xml+xhtml"; - - private static final String LINK_TYPE_RSS = "application/rss+xml"; - - /** - * Regexp to test whether an Element is a Text Element. - */ - private static final String isText = TITLE + "|" + CONTENT + "|" - + SUBTITLE + "|" + SUMMARY; - - private static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; - private static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; - - @Override - public SyndElement handleElementStart(String localName, HandlerState state, - Attributes attributes) { - if (ENTRY.equals(localName)) { - state.setCurrentItem(new FeedItem()); - state.getItems().add(state.getCurrentItem()); - state.getCurrentItem().setFeed(state.getFeed()); - } else if (localName.matches(isText)) { - String type = attributes.getValue(TEXT_TYPE); - return new AtomText(localName, this, type); - } else if (LINK.equals(localName)) { - String href = attributes.getValue(LINK_HREF); - String rel = attributes.getValue(LINK_REL); - SyndElement parent = state.getTagstack().peek(); - if (parent.getName().matches(isFeedItem)) { - if (rel == null || LINK_REL_ALTERNATE.equals(rel)) { - state.getCurrentItem().setLink(href); - } else if (LINK_REL_ENCLOSURE.equals(rel)) { - String strSize = attributes.getValue(LINK_LENGTH); - long size = 0; - try { - if (strSize != null) { - size = Long.parseLong(strSize); - } - } catch (NumberFormatException e) { - Log.d(TAG, "Length attribute could not be parsed."); - } - String type = attributes.getValue(LINK_TYPE); - - if (type == null) { - type = SyndTypeUtils.getMimeTypeFromUrl(href); - } - - FeedItem currItem = state.getCurrentItem(); - if (SyndTypeUtils.enclosureTypeValid(type) && currItem != null && !currItem.hasMedia()) { - currItem.setMedia(new FeedMedia(currItem, href, size, type)); - } - } else if (LINK_REL_PAYMENT.equals(rel)) { - state.getCurrentItem().setPaymentLink(href); - } - } else if (parent.getName().matches(isFeed)) { - if (rel == null || LINK_REL_ALTERNATE.equals(rel)) { - String type = attributes.getValue(LINK_TYPE); - /* - * Use as link if a) no type-attribute is given and - * feed-object has no link yet b) type of link is - * LINK_TYPE_HTML or LINK_TYPE_XHTML - */ - if (state.getFeed() != null && - ((type == null && state.getFeed().getLink() == null) || - (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)))) { - state.getFeed().setLink(href); - } else if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) { - // treat as podlove alternate feed - String title = attributes.getValue(LINK_TITLE); - if (TextUtils.isEmpty(title)) { - title = href; - } - state.addAlternateFeedUrl(title, href); - } - } else if (LINK_REL_ARCHIVES.equals(rel) && state.getFeed() != null) { - String type = attributes.getValue(LINK_TYPE); - if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) { - String title = attributes.getValue(LINK_TITLE); - if (TextUtils.isEmpty(title)) { - title = href; - } - state.addAlternateFeedUrl(title, href); - } else if (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)) { - //A Link such as to a directory such as iTunes - } - } else if (LINK_REL_PAYMENT.equals(rel) && state.getFeed() != null) { - state.getFeed().addPayment(new FeedFunding(href, "")); - } else if (LINK_REL_NEXT.equals(rel) && state.getFeed() != null) { - state.getFeed().setPaged(true); - state.getFeed().setNextPageLink(href); - } - } - } - return new SyndElement(localName, this); - } - - @Override - public void handleElementEnd(String localName, HandlerState state) { - if (ENTRY.equals(localName)) { - if (state.getCurrentItem() != null && - state.getTempObjects().containsKey(NSITunes.DURATION)) { - FeedItem currentItem = state.getCurrentItem(); - if (currentItem.hasMedia()) { - Integer duration = (Integer) state.getTempObjects().get(NSITunes.DURATION); - currentItem.getMedia().setDuration(duration); - } - state.getTempObjects().remove(NSITunes.DURATION); - } - state.setCurrentItem(null); - } - - if (state.getTagstack().size() >= 2) { - AtomText textElement = null; - String contentRaw; - if (state.getContentBuf() != null) { - contentRaw = state.getContentBuf().toString(); - } else { - contentRaw = ""; - } - String content = SyndStringUtils.trimAllWhitespace(contentRaw); - SyndElement topElement = state.getTagstack().peek(); - String top = topElement.getName(); - SyndElement secondElement = state.getSecondTag(); - String second = secondElement.getName(); - - if (top.matches(isText)) { - textElement = (AtomText) topElement; - textElement.setContent(content); - } - - if (ID.equals(top)) { - if (FEED.equals(second) && state.getFeed() != null) { - state.getFeed().setFeedIdentifier(contentRaw); - } else if (ENTRY.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setItemIdentifier(contentRaw); - } - } else if (TITLE.equals(top) && textElement != null) { - if (FEED.equals(second) && state.getFeed() != null) { - state.getFeed().setTitle(textElement.getProcessedContent()); - } else if (ENTRY.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setTitle(textElement.getProcessedContent()); - } - } else if (SUBTITLE.equals(top) && FEED.equals(second) && textElement != null && - state.getFeed() != null) { - state.getFeed().setDescription(textElement.getProcessedContent()); - } else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null && - state.getCurrentItem() != null) { - state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); - } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null - && state.getCurrentItem() != null) { - state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); - } else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null && - state.getCurrentItem().getPubDate() == null) { - state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); - } else if (PUBLISHED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null) { - state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); - } else if (IMAGE_LOGO.equals(top) && state.getFeed() != null && state.getFeed().getImageUrl() == null) { - state.getFeed().setImageUrl(content); - } else if (IMAGE_ICON.equals(top) && state.getFeed() != null) { - state.getFeed().setImageUrl(content); - } else if (AUTHOR_NAME.equals(top) && AUTHOR.equals(second) && - state.getFeed() != null && state.getCurrentItem() == null) { - String currentName = state.getFeed().getAuthor(); - if (currentName == null) { - state.getFeed().setAuthor(content); - } else { - state.getFeed().setAuthor(currentName + ", " + content); - } - } - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/parsers/DurationParser.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/parsers/DurationParser.java deleted file mode 100644 index 8b036c6a9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/parsers/DurationParser.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.danoeh.antennapod.core.syndication.parsers; - -import static java.util.concurrent.TimeUnit.HOURS; -import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.concurrent.TimeUnit.SECONDS; - -public class DurationParser { - public static long inMillis(String durationStr) throws NumberFormatException { - String[] parts = durationStr.trim().split(":"); - - if (parts.length == 1) { - return toMillis(parts[0]); - } else if (parts.length == 2) { - return toMillis("0", parts[0], parts[1]); - } else if (parts.length == 3) { - return toMillis(parts[0], parts[1], parts[2]); - } else { - throw new NumberFormatException(); - } - } - - private static long toMillis(String hours, String minutes, String seconds) { - return HOURS.toMillis(Long.parseLong(hours)) - + MINUTES.toMillis(Long.parseLong(minutes)) - + toMillis(seconds); - } - - private static long toMillis(String seconds) { - if (seconds.contains(".")) { - float value = Float.parseFloat(seconds); - float millis = value % 1; - return SECONDS.toMillis((long) value) + (long) (millis * 1000); - } else { - return SECONDS.toMillis(Long.parseLong(seconds)); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndStringUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndStringUtils.java deleted file mode 100644 index addcdd4b7..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndStringUtils.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danoeh.antennapod.core.syndication.util; - -public class SyndStringUtils { - private SyndStringUtils() { - - } - - /** - * Trims all whitespace from beginning and ending of a String. {{@link String#trim()}} only trims spaces. - */ - public static String trimAllWhitespace(String string) { - return string.replaceAll("(^\\s*)|(\\s*$)", ""); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java deleted file mode 100644 index 155673296..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.danoeh.antennapod.core.syndication.util; - -import android.webkit.MimeTypeMap; -import org.apache.commons.io.FilenameUtils; - -/** - * Utility class for handling MIME-Types of enclosures. - * */ -public class SyndTypeUtils { - private SyndTypeUtils() { - - } - - public static boolean enclosureTypeValid(String type) { - if (type == null) { - return false; - } else { - return type.startsWith("audio/") - || type.startsWith("video/") - || type.equals("application/ogg") - || type.equals("application/octet-stream"); - } - } - - public static boolean imageTypeValid(String type) { - if (type == null) { - return false; - } else { - return type.startsWith("image/"); - } - } - - /** - * Should be used if mime-type of enclosure tag is not supported. This - * method will return the mime-type of the file extension. - */ - public static String getMimeTypeFromUrl(String url) { - if (url == null) { - return null; - } - String extension = FilenameUtils.getExtension(url); - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java new file mode 100644 index 000000000..99628dfcc --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; + +/** + * Formats dates. + */ +public class DateFormatter { + private DateFormatter() { + + } + + public static String formatRfc822Date(Date date) { + SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + return format.format(date); + } + + public static String formatAbbrev(final Context context, final Date date) { + if (date == null) { + return ""; + } + GregorianCalendar now = new GregorianCalendar(); + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(date); + boolean withinLastYear = now.get(Calendar.YEAR) == cal.get(Calendar.YEAR); + int format = android.text.format.DateUtils.FORMAT_ABBREV_ALL; + if (withinLastYear) { + format |= android.text.format.DateUtils.FORMAT_NO_YEAR; + } + return android.text.format.DateUtils.formatDateTime(context, date.getTime(), format); + } + + public static String formatForAccessibility(final Context context, final Date date) { + if (date == null) { + return ""; + } + return DateFormat.getDateInstance(DateFormat.LONG).format(date); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java deleted file mode 100644 index a0b9fbef9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ /dev/null @@ -1,202 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.Nullable; -import org.apache.commons.lang3.StringUtils; - -import java.text.DateFormat; -import java.text.ParsePosition; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Parses several date formats. - */ -public class DateUtils { - - private DateUtils(){} - - private static final String TAG = "DateUtils"; - private static final TimeZone defaultTimezone = TimeZone.getTimeZone("GMT"); - - public static Date parse(final String input) { - if (input == null) { - throw new IllegalArgumentException("Date must not be null"); - } - String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " "); - - // remove colon from timezone to avoid differences between Android and Java SimpleDateFormat - date = date.replaceAll("([+-]\\d\\d):(\\d\\d)$", "$1$2"); - - // CEST is widely used but not in the "ISO 8601 Time zone" list. Let's hack around. - date = date.replaceAll("CEST$", "+0200"); - date = date.replaceAll("CET$", "+0100"); - - // some generators use "Sept" for September - date = date.replaceAll("\\bSept\\b", "Sep"); - - // if datetime is more precise than seconds, make sure the value is in ms - if (date.contains(".")) { - int start = date.indexOf('.'); - int current = start + 1; - while (current < date.length() && Character.isDigit(date.charAt(current))) { - current++; - } - // even more precise than microseconds: discard further decimal places - if (current - start > 4) { - if (current < date.length() - 1) { - date = date.substring(0, start + 4) + date.substring(current); - } else { - date = date.substring(0, start + 4); - } - // less than 4 decimal places: pad to have a consistent format for the parser - } else if (current - start < 4) { - if (current < date.length() - 1) { - date = date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start)) + date.substring(current); - } else { - date = date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start)); - } - } - } - final String[] patterns = { - "dd MMM yy HH:mm:ss Z", - "dd MMM yy HH:mm Z", - "EEE, dd MMM yyyy HH:mm:ss Z", - "EEE, dd MMM yyyy HH:mm:ss", - "EEE, dd MMMM yyyy HH:mm:ss Z", - "EEE, dd MMMM yyyy HH:mm:ss", - "EEEE, dd MMM yyyy HH:mm:ss Z", - "EEEE, dd MMM yy HH:mm:ss Z", - "EEEE, dd MMM yyyy HH:mm:ss", - "EEEE, dd MMM yy HH:mm:ss", - "EEE MMM d HH:mm:ss yyyy", - "EEE, dd MMM yyyy HH:mm Z", - "EEE, dd MMM yyyy HH:mm", - "EEE, dd MMMM yyyy HH:mm Z", - "EEE, dd MMMM yyyy HH:mm", - "EEEE, dd MMM yyyy HH:mm Z", - "EEEE, dd MMM yy HH:mm Z", - "EEEE, dd MMM yyyy HH:mm", - "EEEE, dd MMM yy HH:mm", - "EEE MMM d HH:mm yyyy", - "yyyy-MM-dd'T'HH:mm:ss", - "yyyy-MM-dd'T'HH:mm:ss.SSS Z", - "yyyy-MM-dd'T'HH:mm:ss.SSS", - "yyyy-MM-dd'T'HH:mm:ssZ", - "yyyy-MM-dd'T'HH:mm:ss'Z'", - "yyyy-MM-dd'T'HH:mm:ss.SSSZ", - "yyyy-MM-ddZ", - "yyyy-MM-dd", - "EEE d MMM yyyy HH:mm:ss 'GMT'Z (z)" - }; - - SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); - parser.setLenient(false); - parser.setTimeZone(defaultTimezone); - - ParsePosition pos = new ParsePosition(0); - for (String pattern : patterns) { - parser.applyPattern(pattern); - pos.setIndex(0); - try { - Date result = parser.parse(date, pos); - if (result != null && pos.getIndex() == date.length()) { - return result; - } - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - // if date string starts with a weekday, try parsing date string without it - if (date.matches("^\\w+, .*$")) { - return parse(date.substring(date.indexOf(',') + 1)); - } - - Log.d(TAG, "Could not parse date string \"" + input + "\" [" + date + "]"); - return null; - } - - /** - * Parses the date but if the date is in the future, returns null. - */ - @Nullable - public static Date parseOrNullIfFuture(final String input) { - Date date = parse(input); - if (date == null) { - return null; - } - Date now = new Date(); - if (date.after(now)) { - return null; - } - return date; - } - - /** - * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to - * milliseconds. - * - * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. - */ - public static long parseTimeString(final String time) { - String[] parts = time.split(":"); - long result = 0; - int idx = 0; - if (parts.length == 3) { - // string has hours - result += Integer.parseInt(parts[idx]) * 3600000L; - idx++; - } - if (parts.length >= 2) { - result += Integer.parseInt(parts[idx]) * 60000L; - idx++; - result += (long) (Float.parseFloat(parts[idx]) * 1000L); - } - return result; - } - - public static String formatRFC822Date(Date date) { - SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); - return format.format(date); - } - - public static String formatRFC3339Local(Date date) { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); - return format.format(date); - } - - public static String formatRFC3339UTC(Date date) { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(defaultTimezone); - return format.format(date); - } - - public static String formatAbbrev(final Context context, final Date date) { - if (date == null) { - return ""; - } - GregorianCalendar now = new GregorianCalendar(); - GregorianCalendar cal = new GregorianCalendar(); - cal.setTime(date); - boolean withinLastYear = now.get(Calendar.YEAR) == cal.get(Calendar.YEAR); - int format = android.text.format.DateUtils.FORMAT_ABBREV_ALL; - if (withinLastYear) { - format |= android.text.format.DateUtils.FORMAT_NO_YEAR; - } - return android.text.format.DateUtils.formatDateTime(context, date.getTime(), format); - } - - public static String formatForAccessibility(final Context context, final Date date) { - if (date == null) { - return ""; - } - return DateFormat.getDateInstance(DateFormat.LONG).format(date); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java index 6c1e8d4f9..413243d1d 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTestUtils.java @@ -9,7 +9,7 @@ import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.SimpleChapter; +import de.danoeh.antennapod.parser.feed.element.SimpleChapter; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import static org.junit.Assert.assertTrue; diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java deleted file mode 100644 index 36ca7f0d8..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/AtomParserTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import java.io.File; -import java.util.Date; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; - -/** - * Tests for Atom feeds in FeedHandler. - */ -@RunWith(RobolectricTestRunner.class) -public class AtomParserTest { - - @Test - public void testAtomBasic() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testAtomBasic.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals(Feed.TYPE_ATOM1, feed.getType()); - assertEquals("title", feed.getTitle()); - 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.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++) { - FeedItem item = feed.getItems().get(i); - assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); - assertEquals("item-" + i, item.getTitle()); - assertNull(item.getDescription()); - assertEquals("http://example.com/items/" + i, item.getLink()); - assertEquals(new Date(i * 60000), item.getPubDate()); - assertNull(item.getPaymentLink()); - assertEquals("http://example.com/picture", item.getImageLocation()); - // media - assertTrue(item.hasMedia()); - FeedMedia media = item.getMedia(); - //noinspection ConstantConditions - assertEquals("http://example.com/media-" + i, media.getDownload_url()); - assertEquals(1024 * 1024, media.getSize()); - assertEquals("audio/mp3", media.getMime_type()); - // chapters - assertNull(item.getChapters()); - } - } - - @Test - public void testEmptyRelLinks() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testEmptyRelLinks.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals(Feed.TYPE_ATOM1, feed.getType()); - assertEquals("title", feed.getTitle()); - assertEquals("http://example.com/feed", feed.getFeedIdentifier()); - assertEquals("http://example.com", feed.getLink()); - assertEquals("This is the description", feed.getDescription()); - assertNull(feed.getPaymentLinks()); - assertEquals("http://example.com/picture", feed.getImageUrl()); - assertEquals(1, feed.getItems().size()); - - // feed entry - FeedItem item = feed.getItems().get(0); - assertEquals("http://example.com/item-0", item.getItemIdentifier()); - assertEquals("item-0", item.getTitle()); - assertNull(item.getDescription()); - assertEquals("http://example.com/items/0", item.getLink()); - assertEquals(new Date(0), item.getPubDate()); - assertNull(item.getPaymentLink()); - assertEquals("http://example.com/picture", item.getImageLocation()); - // media - assertFalse(item.hasMedia()); - // chapters - assertNull(item.getChapters()); - } - - @Test - public void testLogoWithWhitespace() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testLogoWithWhitespace.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals("title", feed.getTitle()); - 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.getPaymentLinks().get(0).url); - assertEquals("https://example.com/image.png", feed.getImageUrl()); - assertEquals(0, feed.getItems().size()); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java deleted file mode 100644 index b9318b377..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/FeedParserTestHelper.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import androidx.annotation.NonNull; - -import java.io.File; - -import de.danoeh.antennapod.model.feed.Feed; - -/** - * Tests for FeedHandler. - */ -public abstract class FeedParserTestHelper { - - /** - * Returns the File object for a file in the resources folder. - */ - @NonNull - static File getFeedFile(@NonNull String fileName) { - //noinspection ConstantConditions - return new File(FeedParserTestHelper.class.getClassLoader().getResource(fileName).getFile()); - } - - /** - * Runs the feed parser on the given file. - */ - @NonNull - static Feed runFeedParser(@NonNull File feedFile) throws Exception { - FeedHandler handler = new FeedHandler(); - Feed parsedFeed = new Feed("http://example.com/feed", null); - parsedFeed.setFile_url(feedFile.getAbsolutePath()); - parsedFeed.setDownloaded(true); - handler.parseFeed(parsedFeed); - return parsedFeed; - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java deleted file mode 100644 index d95c8b3ab..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/syndication/handler/RssParserTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.danoeh.antennapod.core.syndication.handler; - -import android.text.TextUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import java.io.File; -import java.util.Date; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.MediaType; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -/** - * Tests for RSS feeds in FeedHandler. - */ -@RunWith(RobolectricTestRunner.class) -public class RssParserTest { - - @Test - public void testRss2Basic() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testRss2Basic.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals(Feed.TYPE_RSS2, feed.getType()); - assertEquals("title", feed.getTitle()); - assertEquals("en", feed.getLanguage()); - assertEquals("http://example.com", feed.getLink()); - assertEquals("This is the description", feed.getDescription()); - 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++) { - FeedItem item = feed.getItems().get(i); - assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); - assertEquals("item-" + i, item.getTitle()); - assertNull(item.getDescription()); - assertEquals("http://example.com/items/" + i, item.getLink()); - assertEquals(new Date(i * 60000), item.getPubDate()); - assertNull(item.getPaymentLink()); - assertEquals("http://example.com/picture", item.getImageLocation()); - // media - assertTrue(item.hasMedia()); - FeedMedia media = item.getMedia(); - //noinspection ConstantConditions - assertEquals("http://example.com/media-" + i, media.getDownload_url()); - assertEquals(1024 * 1024, media.getSize()); - assertEquals("audio/mp3", media.getMime_type()); - // chapters - assertNull(item.getChapters()); - } - } - - @Test - public void testImageWithWhitespace() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testImageWithWhitespace.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals("title", feed.getTitle()); - assertEquals("http://example.com", feed.getLink()); - assertEquals("This is the description", feed.getDescription()); - assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url); - assertEquals("https://example.com/image.png", feed.getImageUrl()); - assertEquals(0, feed.getItems().size()); - } - - @Test - public void testMediaContentMime() throws Exception { - File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testMediaContentMime.xml"); - Feed feed = FeedParserTestHelper.runFeedParser(feedFile); - assertEquals("title", feed.getTitle()); - assertEquals("http://example.com", feed.getLink()); - assertEquals("This is the description", feed.getDescription()); - assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url); - assertNull(feed.getImageUrl()); - assertEquals(1, feed.getItems().size()); - FeedItem feedItem = feed.getItems().get(0); - //noinspection ConstantConditions - 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); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomTextTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomTextTest.java deleted file mode 100644 index 6bc614364..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomTextTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.syndication.namespace.atom; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import static org.junit.Assert.assertEquals; - -/** - * Unit test for {@link AtomText}. - */ -@RunWith(RobolectricTestRunner.class) -public class AtomTextTest { - - private static final String[][] TEST_DATA = { - {">", ">"}, - {">", ">"}, - {"<Français>", ""}, - {"ßÄÖÜ", "ßÄÖÜ"}, - {""", "\""}, - {"ß", "ß"}, - {"’", "’"}, - {"‰", "‰"}, - {"€", "€"} - }; - - @Test - public void testProcessingHtml() { - for (String[] pair : TEST_DATA) { - final AtomText atomText = new AtomText("", new NSAtom(), AtomText.TYPE_HTML); - atomText.setContent(pair[0]); - assertEquals(pair[1], atomText.getProcessedContent()); - } - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/syndication/parsers/DurationParserTest.java b/core/src/test/java/de/danoeh/antennapod/core/syndication/parsers/DurationParserTest.java deleted file mode 100644 index e7c861969..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/syndication/parsers/DurationParserTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.danoeh.antennapod.core.syndication.parsers; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class DurationParserTest { - private int milliseconds = 1; - private int seconds = 1000 * milliseconds; - private int minutes = 60 * seconds; - private int hours = 60 * minutes; - - @Test - public void testSecondDurationInMillis() { - long duration = DurationParser.inMillis("00:45"); - assertEquals(45 * seconds, duration); - } - - @Test - public void testSingleNumberDurationInMillis() { - int twoHoursInSeconds = 2 * 60 * 60; - long duration = DurationParser.inMillis(String.valueOf(twoHoursInSeconds)); - assertEquals(2 * hours, duration); - } - - @Test - public void testMinuteSecondDurationInMillis() { - long duration = DurationParser.inMillis("05:10"); - assertEquals(5 * minutes + 10 * seconds, duration); - } - - @Test - public void testHourMinuteSecondDurationInMillis() { - long duration = DurationParser.inMillis("02:15:45"); - assertEquals(2 * hours + 15 * minutes + 45 * seconds, duration); - } - - @Test - public void testSecondsWithMillisecondsInMillis() { - long duration = DurationParser.inMillis("00:00:00.123"); - assertEquals(123, duration); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java deleted file mode 100644 index 92888ae8b..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/DateUtilsTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import org.junit.Test; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; - -import static org.junit.Assert.assertEquals; - -/** - * Unit test for {@link DateUtils}. - */ -public class DateUtilsTest { - - @Test - public void testParseDateWithMicroseconds() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 963); - Date actual = DateUtils.parse("2015-03-28T13:31:04.963870"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithCentiseconds() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 960); - Date actual = DateUtils.parse("2015-03-28T13:31:04.96"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithDeciseconds() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 900); - Date actual = DateUtils.parse("2015-03-28T13:31:04.9"); - assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); - assertEquals(900, actual.getTime() % 1000); - } - - @Test - public void testParseDateWithMicrosecondsAndTimezone() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 963); - Date actual = DateUtils.parse("2015-03-28T13:31:04.963870 +0700"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithCentisecondsAndTimezone() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 960); - Date actual = DateUtils.parse("2015-03-28T13:31:04.96 +0700"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithDecisecondsAndTimezone() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 900); - Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700"); - assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); - assertEquals(900, actual.getTime() % 1000); - } - - @Test - public void testParseDateWithTimezoneName() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis()); - Date actual = DateUtils.parse("Sat, 28 Mar 2015 01:31:04 EST"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithTimezoneName2() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 0); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis()); - Date actual = DateUtils.parse("Sat, 28 Mar 2015 01:31 EST"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithTimeZoneOffset() { - GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 12, 16, 12); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis()); - Date actual = DateUtils.parse("Sat, 28 March 2015 08:16:12 -0400"); - assertEquals(expected, actual); - } - - @Test - public void testAsctime() { - GregorianCalendar exp = new GregorianCalendar(2011, 4, 25, 12, 33, 0); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis()); - Date actual = DateUtils.parse("Wed, 25 May 2011 12:33:00"); - assertEquals(expected, actual); - } - - @Test - public void testMultipleConsecutiveSpaces() { - GregorianCalendar exp = new GregorianCalendar(2010, 2, 23, 6, 6, 26); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis()); - Date actual = DateUtils.parse("Tue, 23 Mar 2010 01:06:26 -0500"); - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithNoTimezonePadding() { - GregorianCalendar exp = new GregorianCalendar(2017, 1, 22, 22, 28, 0); - exp.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected = new Date(exp.getTimeInMillis() + 2); - Date actual = DateUtils.parse("2017-02-22T14:28:00.002-08:00"); - assertEquals(expected, actual); - } - - /** - * Requires Android platform. Root cause: {@link DateUtils} implementation makes - * use of ISO 8601 time zone, which does not work on standard JDK. - * - * @see #testParseDateWithNoTimezonePadding() - */ - @Test - public void testParseDateWithForCest() { - GregorianCalendar exp1 = new GregorianCalendar(2017, 0, 28, 22, 0, 0); - exp1.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected1 = new Date(exp1.getTimeInMillis()); - Date actual1 = DateUtils.parse("Sun, 29 Jan 2017 00:00:00 CEST"); - assertEquals(expected1, actual1); - - GregorianCalendar exp2 = new GregorianCalendar(2017, 0, 28, 23, 0, 0); - exp2.setTimeZone(TimeZone.getTimeZone("UTC")); - Date expected2 = new Date(exp2.getTimeInMillis()); - Date actual2 = DateUtils.parse("Sun, 29 Jan 2017 00:00:00 CET"); - assertEquals(expected2, actual2); - } - - @Test - public void testParseDateWithIncorrectWeekday() { - GregorianCalendar exp1 = new GregorianCalendar(2014, 9, 8, 9, 0, 0); - exp1.setTimeZone(TimeZone.getTimeZone("GMT")); - Date expected = new Date(exp1.getTimeInMillis()); - Date actual = DateUtils.parse("Thu, 8 Oct 2014 09:00:00 GMT"); // actually a Wednesday - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithBadAbbreviation() { - GregorianCalendar exp1 = new GregorianCalendar(2014, 8, 8, 0, 0, 0); - exp1.setTimeZone(TimeZone.getTimeZone("GMT")); - Date expected = new Date(exp1.getTimeInMillis()); - Date actual = DateUtils.parse("Mon, 8 Sept 2014 00:00:00 GMT"); // should be Sep - assertEquals(expected, actual); - } - - @Test - public void testParseDateWithTwoTimezones() { - final GregorianCalendar exp1 = new GregorianCalendar(2015, Calendar.MARCH, 1, 1, 0, 0); - exp1.setTimeZone(TimeZone.getTimeZone("GMT-4")); - final Date expected = new Date(exp1.getTimeInMillis()); - final Date actual = DateUtils.parse("Sun 01 Mar 2015 01:00:00 GMT-0400 (EDT)"); - assertEquals(expected, actual); - } -} diff --git a/core/src/test/resources/feed-atom-testAtomBasic.xml b/core/src/test/resources/feed-atom-testAtomBasic.xml deleted file mode 100644 index cefc4f979..000000000 --- a/core/src/test/resources/feed-atom-testAtomBasic.xml +++ /dev/null @@ -1 +0,0 @@ -http://example.com/feedtitleThis is the descriptionhttp://example.com/picturehttp://example.com/item-0item-01970-01-01T00:00:00Zhttp://example.com/item-1item-11970-01-01T00:01:00Zhttp://example.com/item-2item-21970-01-01T00:02:00Zhttp://example.com/item-3item-31970-01-01T00:03:00Zhttp://example.com/item-4item-41970-01-01T00:04:00Zhttp://example.com/item-5item-51970-01-01T00:05:00Zhttp://example.com/item-6item-61970-01-01T00:06:00Zhttp://example.com/item-7item-71970-01-01T00:07:00Zhttp://example.com/item-8item-81970-01-01T00:08:00Zhttp://example.com/item-9item-91970-01-01T00:09:00Z \ No newline at end of file diff --git a/core/src/test/resources/feed-atom-testEmptyRelLinks.xml b/core/src/test/resources/feed-atom-testEmptyRelLinks.xml deleted file mode 100644 index 04c28ef67..000000000 --- a/core/src/test/resources/feed-atom-testEmptyRelLinks.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - http://example.com/feed - title - - This is the description - http://example.com/picture - - http://example.com/item-0 - item-0 - - 1970-01-01T00:00:00Z - - diff --git a/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml b/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml deleted file mode 100644 index f4886d56a..000000000 --- a/core/src/test/resources/feed-atom-testLogoWithWhitespace.xml +++ /dev/null @@ -1,2 +0,0 @@ -http://example.com/feedtitleThis is the description https://example.com/image.png - \ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testImageWithWhitespace.xml b/core/src/test/resources/feed-rss-testImageWithWhitespace.xml deleted file mode 100644 index 2be9401d2..000000000 --- a/core/src/test/resources/feed-rss-testImageWithWhitespace.xml +++ /dev/null @@ -1,2 +0,0 @@ -titleThis is the descriptionhttp://example.comen https://example.com/image.png - \ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testMediaContentMime.xml b/core/src/test/resources/feed-rss-testMediaContentMime.xml deleted file mode 100644 index a715abb37..000000000 --- a/core/src/test/resources/feed-rss-testMediaContentMime.xml +++ /dev/null @@ -1 +0,0 @@ -titleThis is the descriptionhttp://example.comen \ No newline at end of file diff --git a/core/src/test/resources/feed-rss-testMultipleFundingTags.xml b/core/src/test/resources/feed-rss-testMultipleFundingTags.xml deleted file mode 100644 index 2535bda32..000000000 --- a/core/src/test/resources/feed-rss-testMultipleFundingTags.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - title - - Text 1 - Text 2 - - diff --git a/core/src/test/resources/feed-rss-testRss2Basic.xml b/core/src/test/resources/feed-rss-testRss2Basic.xml deleted file mode 100644 index dd771b61a..000000000 --- a/core/src/test/resources/feed-rss-testRss2Basic.xml +++ /dev/null @@ -1 +0,0 @@ -titleThis is the descriptionhttp://example.comenhttp://example.com/pictureitem-0http://example.com/items/001 Jan 70 01:00:00 +0100http://example.com/item-0item-1http://example.com/items/101 Jan 70 01:01:00 +0100http://example.com/item-1item-2http://example.com/items/201 Jan 70 01:02:00 +0100http://example.com/item-2item-3http://example.com/items/301 Jan 70 01:03:00 +0100http://example.com/item-3item-4http://example.com/items/401 Jan 70 01:04:00 +0100http://example.com/item-4item-5http://example.com/items/501 Jan 70 01:05:00 +0100http://example.com/item-5item-6http://example.com/items/601 Jan 70 01:06:00 +0100http://example.com/item-6item-7http://example.com/items/701 Jan 70 01:07:00 +0100http://example.com/item-7item-8http://example.com/items/801 Jan 70 01:08:00 +0100http://example.com/item-8item-9http://example.com/items/901 Jan 70 01:09:00 +0100http://example.com/item-9 \ No newline at end of file diff --git a/parser/README.md b/parser/README.md new file mode 100644 index 000000000..c4fe1a933 --- /dev/null +++ b/parser/README.md @@ -0,0 +1,3 @@ +# :parser + +This folder contains modules that parse data, for example XML or media files. diff --git a/parser/feed/README.md b/parser/feed/README.md new file mode 100644 index 000000000..cfda75a4c --- /dev/null +++ b/parser/feed/README.md @@ -0,0 +1,3 @@ +# :parser:feed + +This module provides the XML feed parser. diff --git a/parser/feed/build.gradle b/parser/feed/build.gradle new file mode 100644 index 000000000..3f6ea4aa3 --- /dev/null +++ b/parser/feed/build.gradle @@ -0,0 +1,23 @@ +apply plugin: "com.android.library" +apply from: "../../common.gradle" + +android { + lintOptions { + disable "TrustAllX509TrustManager" + } +} + +dependencies { + implementation project(':model') + + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + + implementation "androidx.core:core:$appcompatVersion" + + implementation "org.apache.commons:commons-lang3:$commonslangVersion" + implementation "commons-io:commons-io:$commonsioVersion" + implementation "org.jsoup:jsoup:$jsoupVersion" + + testImplementation 'junit:junit:4.13' + testImplementation 'org.robolectric:robolectric:4.5-alpha-1' +} diff --git a/parser/feed/src/main/AndroidManifest.xml b/parser/feed/src/main/AndroidManifest.xml new file mode 100644 index 000000000..44b10f29a --- /dev/null +++ b/parser/feed/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java new file mode 100644 index 000000000..c7f5c4f21 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandler.java @@ -0,0 +1,91 @@ +package de.danoeh.antennapod.parser.feed; + +import android.text.TextUtils; +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.util.TypeGetter; +import org.apache.commons.io.input.XmlStreamReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; + +public class FeedHandler { + private static final String TAG = "FeedHandler"; + + public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, + ParserConfigurationException, UnsupportedFeedtypeException { + TypeGetter tg = new TypeGetter(); + TypeGetter.Type type = tg.getType(feed); + SyndHandler handler = new SyndHandler(feed, type); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + SAXParser saxParser = factory.newSAXParser(); + File file = new File(feed.getFile_url()); + Reader inputStreamReader = new XmlStreamReader(file); + InputSource inputSource = new InputSource(inputStreamReader); + + saxParser.parse(inputSource, handler); + inputStreamReader.close(); + feed.setItems(dedupItems(feed.getItems())); + return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); + } + + /** + * For updating items that are stored in the database, see also: DBTasks.searchFeedItemByIdentifyingValue + */ + public static List dedupItems(List items) { + if (items == null) { + return null; + } + List list = new ArrayList<>(items); + Set seen = new HashSet<>(); + Iterator it = list.iterator(); + while (it.hasNext()) { + FeedItem item = it.next(); + if (!TextUtils.isEmpty(item.getItemIdentifier()) && seen.contains(item.getItemIdentifier())) { + Log.d(TAG, "Removing duplicate episode guid " + item.getItemIdentifier()); + it.remove(); + continue; + } + + if (item.getMedia() == null || TextUtils.isEmpty(item.getMedia().getStreamUrl())) { + continue; + } + if (seen.contains(item.getMedia().getStreamUrl())) { + Log.d(TAG, "Removing duplicate episode stream url " + item.getMedia().getStreamUrl()); + it.remove(); + } else { + seen.add(item.getMedia().getStreamUrl()); + if (TextUtils.isEmpty(item.getTitle()) || item.getPubDate() == null) { + continue; + } + if (!seen.contains(item.getTitle() + item.getPubDate().toString())) { + seen.add(item.getTitle() + item.getPubDate().toString()); + } else { + Log.d(TAG, "Removing duplicate episode title and pubDate " + + item.getTitle() + + " " + item.getPubDate()); + it.remove(); + } + } + seen.add(item.getItemIdentifier()); + } + return list; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java new file mode 100644 index 000000000..43b3387a0 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/FeedHandlerResult.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.parser.feed; + +import java.util.Map; + +import de.danoeh.antennapod.model.feed.Feed; + +/** + * Container for results returned by the Feed parser + */ +public class FeedHandlerResult { + + public final Feed feed; + public final Map alternateFeedUrls; + + public FeedHandlerResult(Feed feed, Map alternateFeedUrls) { + this.feed = feed; + this.alternateFeedUrls = alternateFeedUrls; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java new file mode 100644 index 000000000..706a328e8 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/HandlerState.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.parser.feed; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedFunding; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.parser.feed.namespace.Namespace; +import de.danoeh.antennapod.parser.feed.element.SyndElement; + +/** + * Contains all relevant information to describe the current state of a + * SyndHandler. + */ +public class HandlerState { + + /** + * Feed that the Handler is currently processing. + */ + public Feed feed; + /** + * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the + * URL of the feed, the value is the title + */ + public final Map alternateUrls; + private final ArrayList items; + private FeedItem currentItem; + private FeedFunding currentFunding; + final Stack tagstack; + /** + * Namespaces that have been defined so far. + */ + final Map namespaces; + final Stack defaultNamespaces; + /** + * Buffer for saving characters. + */ + protected StringBuilder contentBuf; + + /** + * Temporarily saved objects. + */ + private final Map tempObjects; + + public HandlerState(Feed feed) { + this.feed = feed; + alternateUrls = new HashMap<>(); + items = new ArrayList<>(); + tagstack = new Stack<>(); + namespaces = new HashMap<>(); + defaultNamespaces = new Stack<>(); + tempObjects = new HashMap<>(); + } + + public Feed getFeed() { + return feed; + } + + public ArrayList getItems() { + return items; + } + + public FeedItem getCurrentItem() { + return currentItem; + } + + public Stack getTagstack() { + return tagstack; + } + + public void setFeed(Feed feed) { + this.feed = feed; + } + + public void setCurrentItem(FeedItem currentItem) { + 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. + */ + public SyndElement getSecondTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.peek(); + tagstack.push(top); + return second; + } + + public SyndElement getThirdTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.pop(); + SyndElement third = tagstack.peek(); + tagstack.push(second); + tagstack.push(top); + return third; + } + + public StringBuilder getContentBuf() { + return contentBuf; + } + + public void addAlternateFeedUrl(String title, String url) { + alternateUrls.put(url, title); + } + + public Map getTempObjects() { + return tempObjects; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/SyndHandler.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/SyndHandler.java new file mode 100644 index 000000000..16bbecbb8 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/SyndHandler.java @@ -0,0 +1,139 @@ +package de.danoeh.antennapod.parser.feed; + +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.util.TypeGetter; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.parser.feed.namespace.Content; +import de.danoeh.antennapod.parser.feed.namespace.DublinCore; +import de.danoeh.antennapod.parser.feed.namespace.Itunes; +import de.danoeh.antennapod.parser.feed.namespace.Media; +import de.danoeh.antennapod.parser.feed.namespace.Rss20; +import de.danoeh.antennapod.parser.feed.namespace.SimpleChapters; +import de.danoeh.antennapod.parser.feed.namespace.Namespace; +import de.danoeh.antennapod.parser.feed.namespace.PodcastIndex; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.namespace.Atom; + +/** Superclass for all SAX Handlers which process Syndication formats */ +public class SyndHandler extends DefaultHandler { + private static final String TAG = "SyndHandler"; + private static final String DEFAULT_PREFIX = ""; + public final HandlerState state; + + public SyndHandler(Feed feed, TypeGetter.Type type) { + state = new HandlerState(feed); + if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { + state.defaultNamespaces.push(new Rss20()); + } + } + + @Override + public void startElement(String uri, String localName, String qualifiedName, + Attributes attributes) throws SAXException { + state.contentBuf = new StringBuilder(); + Namespace handler = getHandlingNamespace(uri, qualifiedName); + if (handler != null) { + SyndElement element = handler.handleElementStart(localName, state, + attributes); + state.tagstack.push(element); + + } + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (!state.tagstack.empty()) { + if (state.getTagstack().size() >= 2) { + if (state.contentBuf != null) { + state.contentBuf.append(ch, start, length); + } + } + } + } + + @Override + public void endElement(String uri, String localName, String qualifiedName) + throws SAXException { + Namespace handler = getHandlingNamespace(uri, qualifiedName); + if (handler != null) { + handler.handleElementEnd(localName, state); + state.tagstack.pop(); + + } + state.contentBuf = null; + + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.pop(); + } + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // Find the right namespace + if (!state.namespaces.containsKey(uri)) { + if (uri.equals(Atom.NSURI)) { + if (prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.push(new Atom()); + } else if (prefix.equals(Atom.NSTAG)) { + state.namespaces.put(uri, new Atom()); + Log.d(TAG, "Recognized Atom namespace"); + } + } else if (uri.equals(Content.NSURI) + && prefix.equals(Content.NSTAG)) { + state.namespaces.put(uri, new Content()); + Log.d(TAG, "Recognized Content namespace"); + } else if (uri.equals(Itunes.NSURI) + && prefix.equals(Itunes.NSTAG)) { + state.namespaces.put(uri, new Itunes()); + Log.d(TAG, "Recognized ITunes namespace"); + } else if (uri.equals(SimpleChapters.NSURI) + && prefix.matches(SimpleChapters.NSTAG)) { + state.namespaces.put(uri, new SimpleChapters()); + Log.d(TAG, "Recognized SimpleChapters namespace"); + } else if (uri.equals(Media.NSURI) + && prefix.equals(Media.NSTAG)) { + state.namespaces.put(uri, new Media()); + Log.d(TAG, "Recognized media namespace"); + } else if (uri.equals(DublinCore.NSURI) + && prefix.equals(DublinCore.NSTAG)) { + state.namespaces.put(uri, new DublinCore()); + 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 qualifiedName) { + Namespace handler = state.namespaces.get(uri); + if (handler == null && !state.defaultNamespaces.empty() + && !qualifiedName.contains(":")) { + handler = state.defaultNamespaces.peek(); + } + return handler; + } + + @Override + public void endDocument() throws SAXException { + super.endDocument(); + state.getFeed().setItems(state.getItems()); + } + + public HandlerState getState() { + return state; + } + +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/UnsupportedFeedtypeException.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/UnsupportedFeedtypeException.java new file mode 100644 index 000000000..74c126a50 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/UnsupportedFeedtypeException.java @@ -0,0 +1,45 @@ +package de.danoeh.antennapod.parser.feed; + +import de.danoeh.antennapod.parser.feed.util.TypeGetter; +import de.danoeh.antennapod.parser.feed.util.TypeGetter.Type; + +public class UnsupportedFeedtypeException extends Exception { + private static final long serialVersionUID = 9105878964928170669L; + private final TypeGetter.Type type; + private String rootElement; + private String message = null; + + public UnsupportedFeedtypeException(Type type) { + super(); + this.type = type; + } + + public UnsupportedFeedtypeException(Type type, String rootElement) { + this.type = type; + this.rootElement = rootElement; + } + + public UnsupportedFeedtypeException(String message) { + this.message = message; + type = Type.INVALID; + } + + public TypeGetter.Type getType() { + return type; + } + + public String getRootElement() { + return rootElement; + } + + @Override + public String getMessage() { + if (message != null) { + return message; + } else if (type == TypeGetter.Type.INVALID) { + return "Invalid type"; + } else { + return "Type " + type + " not supported"; + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/AtomText.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/AtomText.java new file mode 100644 index 000000000..8acd9cbb4 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/AtomText.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.parser.feed.element; + +import androidx.core.text.HtmlCompat; + +import de.danoeh.antennapod.parser.feed.namespace.Namespace; + +/** Represents Atom Element which contains text (content, title, summary). */ +public class AtomText extends SyndElement { + public static final String TYPE_HTML = "html"; + private static final String TYPE_XHTML = "xhtml"; + + private final String type; + private String content; + + public AtomText(String name, Namespace namespace, String type) { + super(name, namespace); + this.type = type; + } + + /** Processes the content according to the type and returns it. */ + public String getProcessedContent() { + if (type == null) { + return content; + } else if (type.equals(TYPE_HTML)) { + return HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_LEGACY).toString(); + } else if (type.equals(TYPE_XHTML)) { + return content; + } else { // Handle as text by default + return content; + } + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java new file mode 100644 index 000000000..069e49f09 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SimpleChapter.java @@ -0,0 +1,16 @@ +package de.danoeh.antennapod.parser.feed.element; + +import de.danoeh.antennapod.model.feed.Chapter; + +public class SimpleChapter extends Chapter { + public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; + + public SimpleChapter(long start, String title, String link, String imageUrl) { + super(start, title, link, imageUrl); + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_SIMPLECHAPTER; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SyndElement.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SyndElement.java new file mode 100644 index 000000000..98dbe2801 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/element/SyndElement.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.parser.feed.element; + +import de.danoeh.antennapod.parser.feed.namespace.Namespace; + +/** Defines a XML Element that is pushed on the tagstack */ +public class SyndElement { + private final String name; + private final Namespace namespace; + + public SyndElement(String name, Namespace namespace) { + this.name = name; + this.namespace = namespace; + } + + public Namespace getNamespace() { + return namespace; + } + + public String getName() { + return name; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java new file mode 100644 index 000000000..ef802c355 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Atom.java @@ -0,0 +1,224 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import android.text.TextUtils; +import android.util.Log; + +import de.danoeh.antennapod.model.feed.FeedFunding; +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.AtomText; +import de.danoeh.antennapod.parser.feed.util.DateUtils; +import de.danoeh.antennapod.parser.feed.util.SyndStringUtils; +import org.xml.sax.Attributes; + +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils; + +public class Atom extends Namespace { + private static final String TAG = "NSAtom"; + public static final String NSTAG = "atom"; + public static final String NSURI = "http://www.w3.org/2005/Atom"; + + private static final String FEED = "feed"; + private static final String ID = "id"; + private static final String TITLE = "title"; + private static final String ENTRY = "entry"; + private static final String LINK = "link"; + private static final String UPDATED = "updated"; + private static final String AUTHOR = "author"; + private static final String AUTHOR_NAME = "name"; + private static final String CONTENT = "content"; + private static final String SUMMARY = "summary"; + private static final String IMAGE_LOGO = "logo"; + private static final String IMAGE_ICON = "icon"; + private static final String SUBTITLE = "subtitle"; + private static final String PUBLISHED = "published"; + + private static final String TEXT_TYPE = "type"; + // Link + private static final String LINK_HREF = "href"; + private static final String LINK_REL = "rel"; + private static final String LINK_TYPE = "type"; + private static final String LINK_TITLE = "title"; + private static final String LINK_LENGTH = "length"; + // rel-values + private static final String LINK_REL_ALTERNATE = "alternate"; + private static final String LINK_REL_ARCHIVES = "archives"; + private static final String LINK_REL_ENCLOSURE = "enclosure"; + private static final String LINK_REL_PAYMENT = "payment"; + private static final String LINK_REL_NEXT = "next"; + // type-values + private static final String LINK_TYPE_ATOM = "application/atom+xml"; + private static final String LINK_TYPE_HTML = "text/html"; + private static final String LINK_TYPE_XHTML = "application/xml+xhtml"; + + private static final String LINK_TYPE_RSS = "application/rss+xml"; + + /** + * Regexp to test whether an Element is a Text Element. + */ + private static final String isText = TITLE + "|" + CONTENT + "|" + + SUBTITLE + "|" + SUMMARY; + + private static final String isFeed = FEED + "|" + Rss20.CHANNEL; + private static final String isFeedItem = ENTRY + "|" + Rss20.ITEM; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (ENTRY.equals(localName)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + } else if (localName.matches(isText)) { + String type = attributes.getValue(TEXT_TYPE); + return new AtomText(localName, this, type); + } else if (LINK.equals(localName)) { + String href = attributes.getValue(LINK_HREF); + String rel = attributes.getValue(LINK_REL); + SyndElement parent = state.getTagstack().peek(); + if (parent.getName().matches(isFeedItem)) { + if (rel == null || LINK_REL_ALTERNATE.equals(rel)) { + state.getCurrentItem().setLink(href); + } else if (LINK_REL_ENCLOSURE.equals(rel)) { + String strSize = attributes.getValue(LINK_LENGTH); + long size = 0; + try { + if (strSize != null) { + size = Long.parseLong(strSize); + } + } catch (NumberFormatException e) { + Log.d(TAG, "Length attribute could not be parsed."); + } + String type = attributes.getValue(LINK_TYPE); + + if (type == null) { + type = SyndTypeUtils.getMimeTypeFromUrl(href); + } + + FeedItem currItem = state.getCurrentItem(); + if (SyndTypeUtils.enclosureTypeValid(type) && currItem != null && !currItem.hasMedia()) { + currItem.setMedia(new FeedMedia(currItem, href, size, type)); + } + } else if (LINK_REL_PAYMENT.equals(rel)) { + state.getCurrentItem().setPaymentLink(href); + } + } else if (parent.getName().matches(isFeed)) { + if (rel == null || LINK_REL_ALTERNATE.equals(rel)) { + String type = attributes.getValue(LINK_TYPE); + /* + * Use as link if a) no type-attribute is given and + * feed-object has no link yet b) type of link is + * LINK_TYPE_HTML or LINK_TYPE_XHTML + */ + if (state.getFeed() != null && + ((type == null && state.getFeed().getLink() == null) || + (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)))) { + state.getFeed().setLink(href); + } else if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) { + // treat as podlove alternate feed + String title = attributes.getValue(LINK_TITLE); + if (TextUtils.isEmpty(title)) { + title = href; + } + state.addAlternateFeedUrl(title, href); + } + } else if (LINK_REL_ARCHIVES.equals(rel) && state.getFeed() != null) { + String type = attributes.getValue(LINK_TYPE); + if (LINK_TYPE_ATOM.equals(type) || LINK_TYPE_RSS.equals(type)) { + String title = attributes.getValue(LINK_TITLE); + if (TextUtils.isEmpty(title)) { + title = href; + } + state.addAlternateFeedUrl(title, href); + } else if (LINK_TYPE_HTML.equals(type) || LINK_TYPE_XHTML.equals(type)) { + //A Link such as to a directory such as iTunes + } + } else if (LINK_REL_PAYMENT.equals(rel) && state.getFeed() != null) { + state.getFeed().addPayment(new FeedFunding(href, "")); + } else if (LINK_REL_NEXT.equals(rel) && state.getFeed() != null) { + state.getFeed().setPaged(true); + state.getFeed().setNextPageLink(href); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (ENTRY.equals(localName)) { + if (state.getCurrentItem() != null && + state.getTempObjects().containsKey(Itunes.DURATION)) { + FeedItem currentItem = state.getCurrentItem(); + if (currentItem.hasMedia()) { + Integer duration = (Integer) state.getTempObjects().get(Itunes.DURATION); + currentItem.getMedia().setDuration(duration); + } + state.getTempObjects().remove(Itunes.DURATION); + } + state.setCurrentItem(null); + } + + if (state.getTagstack().size() >= 2) { + AtomText textElement = null; + String contentRaw; + if (state.getContentBuf() != null) { + contentRaw = state.getContentBuf().toString(); + } else { + contentRaw = ""; + } + String content = SyndStringUtils.trimAllWhitespace(contentRaw); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + + if (top.matches(isText)) { + textElement = (AtomText) topElement; + textElement.setContent(content); + } + + if (ID.equals(top)) { + if (FEED.equals(second) && state.getFeed() != null) { + state.getFeed().setFeedIdentifier(contentRaw); + } else if (ENTRY.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setItemIdentifier(contentRaw); + } + } else if (TITLE.equals(top) && textElement != null) { + if (FEED.equals(second) && state.getFeed() != null) { + state.getFeed().setTitle(textElement.getProcessedContent()); + } else if (ENTRY.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setTitle(textElement.getProcessedContent()); + } + } else if (SUBTITLE.equals(top) && FEED.equals(second) && textElement != null && + state.getFeed() != null) { + state.getFeed().setDescription(textElement.getProcessedContent()); + } else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null && + state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); + } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null + && state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent()); + } else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null && + state.getCurrentItem().getPubDate() == null) { + state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); + } else if (PUBLISHED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); + } else if (IMAGE_LOGO.equals(top) && state.getFeed() != null && state.getFeed().getImageUrl() == null) { + state.getFeed().setImageUrl(content); + } else if (IMAGE_ICON.equals(top) && state.getFeed() != null) { + state.getFeed().setImageUrl(content); + } else if (AUTHOR_NAME.equals(top) && AUTHOR.equals(second) && + state.getFeed() != null && state.getCurrentItem() == null) { + String currentName = state.getFeed().getAuthor(); + if (currentName == null) { + state.getFeed().setAuthor(content); + } else { + state.getFeed().setAuthor(currentName + ", " + content); + } + } + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Content.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Content.java new file mode 100644 index 000000000..3a7d5ac3a --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Content.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import org.xml.sax.Attributes; + +public class Content extends Namespace { + public static final String NSTAG = "content"; + public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; + + private static final String ENCODED = "encoded"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (ENCODED.equals(localName) && state.getCurrentItem() != null && state.getContentBuf() != null) { + state.getCurrentItem().setDescriptionIfLonger(state.getContentBuf().toString()); + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java new file mode 100644 index 000000000..003f72e9b --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/DublinCore.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.util.DateUtils; +import org.xml.sax.Attributes; + +import de.danoeh.antennapod.model.feed.FeedItem; + +public class DublinCore extends Namespace { + private static final String TAG = "NSDublinCore"; + public static final String NSTAG = "dc"; + public static final String NSURI = "http://purl.org/dc/elements/1.1/"; + + private static final String ITEM = "item"; + private static final String DATE = "date"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (state.getCurrentItem() != null && state.getContentBuf() != null && + state.getTagstack() != null && state.getTagstack().size() >= 2) { + FeedItem currentItem = state.getCurrentItem(); + String top = state.getTagstack().peek().getName(); + String second = state.getSecondTag().getName(); + if (DATE.equals(top) && ITEM.equals(second)) { + String content = state.getContentBuf().toString(); + currentItem.setPubDate(DateUtils.parseOrNullIfFuture(content)); + } + } + } + +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java new file mode 100644 index 000000000..5f47f8377 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Itunes.java @@ -0,0 +1,114 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.text.HtmlCompat; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.util.DurationParser; +import org.xml.sax.Attributes; + + +public class Itunes extends Namespace { + + public static final String NSTAG = "itunes"; + public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; + + private static final String IMAGE = "image"; + private static final String IMAGE_HREF = "href"; + + private static final String AUTHOR = "author"; + public static final String DURATION = "duration"; + private static final String SUBTITLE = "subtitle"; + private static final String SUMMARY = "summary"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (IMAGE.equals(localName)) { + String url = attributes.getValue(IMAGE_HREF); + + if (state.getCurrentItem() != null) { + state.getCurrentItem().setImageUrl(url); + } else { + // this is the feed image + // prefer to all other images + if (!TextUtils.isEmpty(url)) { + state.getFeed().setImageUrl(url); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (state.getContentBuf() == null) { + return; + } + + if (AUTHOR.equals(localName)) { + parseAuthor(state); + } else if (DURATION.equals(localName)) { + parseDuration(state); + } else if (SUBTITLE.equals(localName)) { + parseSubtitle(state); + } else if (SUMMARY.equals(localName)) { + SyndElement secondElement = state.getSecondTag(); + parseSummary(state, secondElement.getName()); + } + } + + private void parseAuthor(HandlerState state) { + if (state.getFeed() != null) { + String author = state.getContentBuf().toString(); + state.getFeed().setAuthor(HtmlCompat.fromHtml(author, + HtmlCompat.FROM_HTML_MODE_LEGACY).toString()); + } + } + + private void parseDuration(HandlerState state) { + String durationStr = state.getContentBuf().toString(); + if (TextUtils.isEmpty(durationStr)) { + return; + } + + try { + long durationMs = DurationParser.inMillis(durationStr); + state.getTempObjects().put(DURATION, (int) durationMs); + } catch (NumberFormatException e) { + Log.e(NSTAG, String.format("Duration '%s' could not be parsed", durationStr)); + } + } + + private void parseSubtitle(HandlerState state) { + String subtitle = state.getContentBuf().toString(); + if (TextUtils.isEmpty(subtitle)) { + return; + } + if (state.getCurrentItem() != null) { + if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) { + state.getCurrentItem().setDescriptionIfLonger(subtitle); + } + } else { + if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) { + state.getFeed().setDescription(subtitle); + } + } + } + + private void parseSummary(HandlerState state, String secondElementName) { + String summary = state.getContentBuf().toString(); + if (TextUtils.isEmpty(summary)) { + return; + } + + if (state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(summary); + } else if (Rss20.CHANNEL.equals(secondElementName) && state.getFeed() != null) { + state.getFeed().setDescription(summary); + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java new file mode 100644 index 000000000..f480a0417 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Media.java @@ -0,0 +1,133 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import android.text.TextUtils; +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import org.xml.sax.Attributes; + +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.parser.feed.element.AtomText; +import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils; + +/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ +public class Media extends Namespace { + private static final String TAG = "NSMedia"; + + public static final String NSTAG = "media"; + public static final String NSURI = "http://search.yahoo.com/mrss/"; + + private static final String CONTENT = "content"; + private static final String DOWNLOAD_URL = "url"; + private static final String SIZE = "fileSize"; + private static final String MIME_TYPE = "type"; + private static final String DURATION = "duration"; + private static final String DEFAULT = "isDefault"; + private static final String MEDIUM = "medium"; + + private static final String MEDIUM_IMAGE = "image"; + private static final String MEDIUM_AUDIO = "audio"; + private static final String MEDIUM_VIDEO = "video"; + + private static final String IMAGE = "thumbnail"; + private static final String IMAGE_URL = "url"; + + private static final String DESCRIPTION = "description"; + private static final String DESCRIPTION_TYPE = "type"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (CONTENT.equals(localName)) { + String url = attributes.getValue(DOWNLOAD_URL); + String type = attributes.getValue(MIME_TYPE); + String defaultStr = attributes.getValue(DEFAULT); + String medium = attributes.getValue(MEDIUM); + boolean validTypeMedia = false; + boolean validTypeImage = false; + boolean isDefault = "true".equals(defaultStr); + String guessedType = SyndTypeUtils.getMimeTypeFromUrl(url); + + if (MEDIUM_AUDIO.equals(medium)) { + validTypeMedia = true; + type = "audio/*"; + } else if (MEDIUM_VIDEO.equals(medium)) { + validTypeMedia = true; + type = "video/*"; + } else if (MEDIUM_IMAGE.equals(medium) && (guessedType == null + || (!guessedType.startsWith("audio/") && !guessedType.startsWith("video/")))) { + // Apparently, some publishers explicitly specify the audio file as an image + validTypeImage = true; + type = "image/*"; + } else { + if (type == null) { + type = guessedType; + } + + if (SyndTypeUtils.enclosureTypeValid(type)) { + validTypeMedia = true; + } else if (SyndTypeUtils.imageTypeValid(type)) { + validTypeImage = true; + } + } + + if (state.getCurrentItem() != null && (state.getCurrentItem().getMedia() == null || isDefault) + && url != null && validTypeMedia) { + long size = 0; + String sizeStr = attributes.getValue(SIZE); + try { + size = Long.parseLong(sizeStr); + } catch (NumberFormatException e) { + Log.e(TAG, "Size \"" + sizeStr + "\" could not be parsed."); + } + + int durationMs = 0; + String durationStr = attributes.getValue(DURATION); + if (!TextUtils.isEmpty(durationStr)) { + try { + long duration = Long.parseLong(durationStr); + durationMs = (int) TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS); + } catch (NumberFormatException e) { + Log.e(TAG, "Duration \"" + durationStr + "\" could not be parsed"); + } + } + FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type); + if (durationMs > 0) { + media.setDuration(durationMs); + } + state.getCurrentItem().setMedia(media); + } else if (state.getCurrentItem() != null && url != null && validTypeImage) { + state.getCurrentItem().setImageUrl(url); + } + } else if (IMAGE.equals(localName)) { + String url = attributes.getValue(IMAGE_URL); + if (url != null) { + if (state.getCurrentItem() != null) { + state.getCurrentItem().setImageUrl(url); + } else { + if (state.getFeed().getImageUrl() == null) { + state.getFeed().setImageUrl(url); + } + } + } + } else if (DESCRIPTION.equals(localName)) { + String type = attributes.getValue(DESCRIPTION_TYPE); + return new AtomText(localName, this, type); + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (DESCRIPTION.equals(localName)) { + String content = state.getContentBuf().toString(); + if (state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(content); + } + } + } +} + diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java new file mode 100644 index 000000000..5273c6731 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Namespace.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import org.xml.sax.Attributes; + +public abstract class Namespace { + public static final String NSTAG = null; + public static final String NSURI = null; + + /** Called by a Feedhandler when in startElement and it detects a namespace element + * @return The SyndElement to push onto the stack + * */ + public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); + + /** Called by a Feedhandler when in endElement and it detects a namespace element + * */ + public abstract void handleElementEnd(String localName, HandlerState state); +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java new file mode 100644 index 000000000..1d4a91192 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/PodcastIndex.java @@ -0,0 +1,39 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import org.jsoup.helper.StringUtil; +import org.xml.sax.Attributes; +import de.danoeh.antennapod.model.feed.FeedFunding; + +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); + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java new file mode 100644 index 000000000..a49cd16dd --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/Rss20.java @@ -0,0 +1,148 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import android.text.TextUtils; +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.util.DateUtils; +import de.danoeh.antennapod.parser.feed.util.SyndStringUtils; +import org.xml.sax.Attributes; + +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.parser.feed.util.SyndTypeUtils; + +import java.util.Locale; + +/** + * SAX-Parser for reading RSS-Feeds. + */ +public class Rss20 extends Namespace { + + private static final String TAG = "NSRSS20"; + + public static final String CHANNEL = "channel"; + public static final String ITEM = "item"; + private static final String GUID = "guid"; + private static final String TITLE = "title"; + private static final String LINK = "link"; + private static final String DESCR = "description"; + private static final String PUBDATE = "pubDate"; + private static final String ENCLOSURE = "enclosure"; + private static final String IMAGE = "image"; + private static final String URL = "url"; + private static final String LANGUAGE = "language"; + + private static final String ENC_URL = "url"; + private static final String ENC_LEN = "length"; + private static final String ENC_TYPE = "type"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (ITEM.equals(localName)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + + } else if (ENCLOSURE.equals(localName)) { + String type = attributes.getValue(ENC_TYPE); + String url = attributes.getValue(ENC_URL); + + boolean validType = SyndTypeUtils.enclosureTypeValid(type); + if (!validType) { + type = SyndTypeUtils.getMimeTypeFromUrl(url); + validType = SyndTypeUtils.enclosureTypeValid(type); + } + + boolean validUrl = !TextUtils.isEmpty(url); + if (state.getCurrentItem() != null && state.getCurrentItem().getMedia() == null + && validType && validUrl) { + long size = 0; + try { + size = Long.parseLong(attributes.getValue(ENC_LEN)); + if (size < 16384) { + // less than 16kb is suspicious, check manually + size = 0; + } + } catch (NumberFormatException e) { + Log.d(TAG, "Length attribute could not be parsed."); + } + FeedMedia media = new FeedMedia(state.getCurrentItem(), url, size, type); + state.getCurrentItem().setMedia(media); + } + + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (ITEM.equals(localName)) { + if (state.getCurrentItem() != null) { + FeedItem currentItem = state.getCurrentItem(); + // the title tag is optional in RSS 2.0. The description is used + // as a title if the item has no title-tag. + if (currentItem.getTitle() == null) { + currentItem.setTitle(currentItem.getDescription()); + } + + if (state.getTempObjects().containsKey(Itunes.DURATION)) { + if (currentItem.hasMedia()) { + Integer duration = (Integer) state.getTempObjects().get(Itunes.DURATION); + currentItem.getMedia().setDuration(duration); + } + state.getTempObjects().remove(Itunes.DURATION); + } + } + state.setCurrentItem(null); + } else if (state.getTagstack().size() >= 2 && state.getContentBuf() != null) { + String contentRaw = state.getContentBuf().toString(); + String content = SyndStringUtils.trimAllWhitespace(contentRaw); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + String third = null; + if (state.getTagstack().size() >= 3) { + third = state.getThirdTag().getName(); + } + if (GUID.equals(top) && ITEM.equals(second)) { + // some feed creators include an empty or non-standard guid-element in their feed, + // which should be ignored + if (!TextUtils.isEmpty(contentRaw) && state.getCurrentItem() != null) { + state.getCurrentItem().setItemIdentifier(contentRaw); + } + } else if (TITLE.equals(top)) { + if (ITEM.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setTitle(content); + } else if (CHANNEL.equals(second) && state.getFeed() != null) { + state.getFeed().setTitle(content); + } + } else if (LINK.equals(top)) { + if (CHANNEL.equals(second) && state.getFeed() != null) { + state.getFeed().setLink(content); + } else if (ITEM.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setLink(content); + } + } else if (PUBDATE.equals(top) && ITEM.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setPubDate(DateUtils.parseOrNullIfFuture(content)); + } else if (URL.equals(top) && IMAGE.equals(second) && CHANNEL.equals(third)) { + // prefer itunes:image + if (state.getFeed() != null && state.getFeed().getImageUrl() == null) { + state.getFeed().setImageUrl(content); + } + } else if (DESCR.equals(localName)) { + if (CHANNEL.equals(second) && state.getFeed() != null) { + state.getFeed().setDescription(content); + } else if (ITEM.equals(second) && state.getCurrentItem() != null) { + state.getCurrentItem().setDescriptionIfLonger(content); + } + } else if (LANGUAGE.equals(localName) && state.getFeed() != null) { + state.getFeed().setLanguage(content.toLowerCase(Locale.US)); + } + } + } + +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java new file mode 100644 index 000000000..e1912ed45 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/namespace/SimpleChapters.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.parser.feed.namespace; + +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.HandlerState; +import de.danoeh.antennapod.parser.feed.element.SimpleChapter; +import de.danoeh.antennapod.parser.feed.element.SyndElement; +import de.danoeh.antennapod.parser.feed.util.DateUtils; +import org.xml.sax.Attributes; + +import java.util.ArrayList; + +import de.danoeh.antennapod.model.feed.FeedItem; + +public class SimpleChapters extends Namespace { + private static final String TAG = "NSSimpleChapters"; + + public static final String NSTAG = "psc|sc"; + public static final String NSURI = "http://podlove.org/simple-chapters"; + + private static final String CHAPTERS = "chapters"; + private static final String CHAPTER = "chapter"; + private static final String START = "start"; + private static final String TITLE = "title"; + private static final String HREF = "href"; + private static final String IMAGE = "image"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) { + FeedItem currentItem = state.getCurrentItem(); + if (currentItem != null) { + if (localName.equals(CHAPTERS)) { + currentItem.setChapters(new ArrayList<>()); + } else if (localName.equals(CHAPTER)) { + try { + long start = DateUtils.parseTimeString(attributes.getValue(START)); + String title = attributes.getValue(TITLE); + String link = attributes.getValue(HREF); + String imageUrl = attributes.getValue(IMAGE); + SimpleChapter chapter = new SimpleChapter(start, title, link, imageUrl); + currentItem.getChapters().add(chapter); + } catch (NumberFormatException e) { + Log.e(TAG, "Unable to read chapter", e); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + } + +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DateUtils.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DateUtils.java new file mode 100644 index 000000000..9b7f48769 --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DateUtils.java @@ -0,0 +1,163 @@ +package de.danoeh.antennapod.parser.feed.util; + +import android.util.Log; + +import androidx.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; + +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Parses several date formats. + */ +public class DateUtils { + + private DateUtils() { + + } + + private static final String TAG = "DateUtils"; + private static final TimeZone defaultTimezone = TimeZone.getTimeZone("GMT"); + + public static Date parse(final String input) { + if (input == null) { + throw new IllegalArgumentException("Date must not be null"); + } + String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " "); + + // remove colon from timezone to avoid differences between Android and Java SimpleDateFormat + date = date.replaceAll("([+-]\\d\\d):(\\d\\d)$", "$1$2"); + + // CEST is widely used but not in the "ISO 8601 Time zone" list. Let's hack around. + date = date.replaceAll("CEST$", "+0200"); + date = date.replaceAll("CET$", "+0100"); + + // some generators use "Sept" for September + date = date.replaceAll("\\bSept\\b", "Sep"); + + // if datetime is more precise than seconds, make sure the value is in ms + if (date.contains(".")) { + int start = date.indexOf('.'); + int current = start + 1; + while (current < date.length() && Character.isDigit(date.charAt(current))) { + current++; + } + // even more precise than microseconds: discard further decimal places + if (current - start > 4) { + if (current < date.length() - 1) { + date = date.substring(0, start + 4) + date.substring(current); + } else { + date = date.substring(0, start + 4); + } + // less than 4 decimal places: pad to have a consistent format for the parser + } else if (current - start < 4) { + if (current < date.length() - 1) { + date = date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start)) + + date.substring(current); + } else { + date = date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start)); + } + } + } + final String[] patterns = { + "dd MMM yy HH:mm:ss Z", + "dd MMM yy HH:mm Z", + "EEE, dd MMM yyyy HH:mm:ss Z", + "EEE, dd MMM yyyy HH:mm:ss", + "EEE, dd MMMM yyyy HH:mm:ss Z", + "EEE, dd MMMM yyyy HH:mm:ss", + "EEEE, dd MMM yyyy HH:mm:ss Z", + "EEEE, dd MMM yy HH:mm:ss Z", + "EEEE, dd MMM yyyy HH:mm:ss", + "EEEE, dd MMM yy HH:mm:ss", + "EEE MMM d HH:mm:ss yyyy", + "EEE, dd MMM yyyy HH:mm Z", + "EEE, dd MMM yyyy HH:mm", + "EEE, dd MMMM yyyy HH:mm Z", + "EEE, dd MMMM yyyy HH:mm", + "EEEE, dd MMM yyyy HH:mm Z", + "EEEE, dd MMM yy HH:mm Z", + "EEEE, dd MMM yyyy HH:mm", + "EEEE, dd MMM yy HH:mm", + "EEE MMM d HH:mm yyyy", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS Z", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ssZ", + "yyyy-MM-dd'T'HH:mm:ss'Z'", + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", + "yyyy-MM-ddZ", + "yyyy-MM-dd", + "EEE d MMM yyyy HH:mm:ss 'GMT'Z (z)" + }; + + SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); + parser.setLenient(false); + parser.setTimeZone(defaultTimezone); + + ParsePosition pos = new ParsePosition(0); + for (String pattern : patterns) { + parser.applyPattern(pattern); + pos.setIndex(0); + try { + Date result = parser.parse(date, pos); + if (result != null && pos.getIndex() == date.length()) { + return result; + } + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } + + // if date string starts with a weekday, try parsing date string without it + if (date.matches("^\\w+, .*$")) { + return parse(date.substring(date.indexOf(',') + 1)); + } + + Log.d(TAG, "Could not parse date string \"" + input + "\" [" + date + "]"); + return null; + } + + /** + * Parses the date but if the date is in the future, returns null. + */ + @Nullable + public static Date parseOrNullIfFuture(final String input) { + Date date = parse(input); + if (date == null) { + return null; + } + Date now = new Date(); + if (date.after(now)) { + return null; + } + return date; + } + + /** + * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to + * milliseconds. + * + * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. + */ + public static long parseTimeString(final String time) { + String[] parts = time.split(":"); + long result = 0; + int idx = 0; + if (parts.length == 3) { + // string has hours + result += Integer.parseInt(parts[idx]) * 3600000L; + idx++; + } + if (parts.length >= 2) { + result += Integer.parseInt(parts[idx]) * 60000L; + idx++; + result += (long) (Float.parseFloat(parts[idx]) * 1000L); + } + return result; + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DurationParser.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DurationParser.java new file mode 100644 index 000000000..af79f542a --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/DurationParser.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.parser.feed.util; + +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class DurationParser { + public static long inMillis(String durationStr) throws NumberFormatException { + String[] parts = durationStr.trim().split(":"); + + if (parts.length == 1) { + return toMillis(parts[0]); + } else if (parts.length == 2) { + return toMillis("0", parts[0], parts[1]); + } else if (parts.length == 3) { + return toMillis(parts[0], parts[1], parts[2]); + } else { + throw new NumberFormatException(); + } + } + + private static long toMillis(String hours, String minutes, String seconds) { + return HOURS.toMillis(Long.parseLong(hours)) + + MINUTES.toMillis(Long.parseLong(minutes)) + + toMillis(seconds); + } + + private static long toMillis(String seconds) { + if (seconds.contains(".")) { + float value = Float.parseFloat(seconds); + float millis = value % 1; + return SECONDS.toMillis((long) value) + (long) (millis * 1000); + } else { + return SECONDS.toMillis(Long.parseLong(seconds)); + } + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndStringUtils.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndStringUtils.java new file mode 100644 index 000000000..403d1671f --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndStringUtils.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.parser.feed.util; + +public class SyndStringUtils { + private SyndStringUtils() { + + } + + /** + * Trims all whitespace from beginning and ending of a String. {{@link String#trim()}} only trims spaces. + */ + public static String trimAllWhitespace(String string) { + return string.replaceAll("(^\\s*)|(\\s*$)", ""); + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java new file mode 100644 index 000000000..2e6cf864f --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/SyndTypeUtils.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.parser.feed.util; + +import android.webkit.MimeTypeMap; +import org.apache.commons.io.FilenameUtils; + +/** + * Utility class for handling MIME-Types of enclosures. + * */ +public class SyndTypeUtils { + private SyndTypeUtils() { + + } + + public static boolean enclosureTypeValid(String type) { + if (type == null) { + return false; + } else { + return type.startsWith("audio/") + || type.startsWith("video/") + || type.equals("application/ogg") + || type.equals("application/octet-stream"); + } + } + + public static boolean imageTypeValid(String type) { + if (type == null) { + return false; + } else { + return type.startsWith("image/"); + } + } + + /** + * Should be used if mime-type of enclosure tag is not supported. This + * method will return the mime-type of the file extension. + */ + public static String getMimeTypeFromUrl(String url) { + if (url == null) { + return null; + } + String extension = FilenameUtils.getExtension(url); + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } +} diff --git a/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java new file mode 100644 index 000000000..12834f94f --- /dev/null +++ b/parser/feed/src/main/java/de/danoeh/antennapod/parser/feed/util/TypeGetter.java @@ -0,0 +1,121 @@ +package de.danoeh.antennapod.parser.feed.util; + +import android.util.Log; + +import de.danoeh.antennapod.parser.feed.UnsupportedFeedtypeException; +import org.apache.commons.io.input.XmlStreamReader; +import org.jsoup.Jsoup; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; + +import de.danoeh.antennapod.model.feed.Feed; + +/** Gets the type of a specific feed by reading the root element. */ +public class TypeGetter { + private static final String TAG = "TypeGetter"; + + public enum Type { + RSS20, RSS091, ATOM, INVALID + } + + private static final String ATOM_ROOT = "feed"; + private static final String RSS_ROOT = "rss"; + + public Type getType(Feed feed) throws UnsupportedFeedtypeException { + XmlPullParserFactory factory; + if (feed.getFile_url() != null) { + Reader reader = null; + try { + factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + reader = createReader(feed); + xpp.setInput(reader); + int eventType = xpp.getEventType(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + String tag = xpp.getName(); + switch (tag) { + case ATOM_ROOT: + feed.setType(Feed.TYPE_ATOM1); + Log.d(TAG, "Recognized type Atom"); + + String strLang = xpp.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang"); + if (strLang != null) { + feed.setLanguage(strLang); + } + + return Type.ATOM; + case RSS_ROOT: + String strVersion = xpp.getAttributeValue(null, "version"); + if (strVersion == null) { + feed.setType(Feed.TYPE_RSS2); + Log.d(TAG, "Assuming type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("2.0")) { + feed.setType(Feed.TYPE_RSS2); + Log.d(TAG, "Recognized type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("0.91") || strVersion.equals("0.92")) { + Log.d(TAG, "Recognized type RSS 0.91/0.92"); + return Type.RSS091; + } + throw new UnsupportedFeedtypeException("Unsupported rss version"); + default: + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID, tag); + } + } else { + eventType = xpp.next(); + } + } + } catch (XmlPullParserException e) { + e.printStackTrace(); + // XML document might actually be a HTML document -> try to parse as HTML + String rootElement = null; + try { + if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { + rootElement = "html"; + } + } catch (IOException e1) { + e1.printStackTrace(); + } + throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID); + } + + private Reader createReader(Feed feed) { + Reader reader; + try { + reader = new XmlStreamReader(new File(feed.getFile_url())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return reader; + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/element/AtomTextTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/element/AtomTextTest.java new file mode 100644 index 000000000..2ec91ab1d --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/element/AtomTextTest.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.parser.feed.element.element; + +import de.danoeh.antennapod.parser.feed.element.AtomText; +import de.danoeh.antennapod.parser.feed.namespace.Atom; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; + +/** + * Unit test for {@link AtomText}. + */ +@RunWith(RobolectricTestRunner.class) +public class AtomTextTest { + + private static final String[][] TEST_DATA = { + {">", ">"}, + {">", ">"}, + {"<Français>", ""}, + {"ßÄÖÜ", "ßÄÖÜ"}, + {""", "\""}, + {"ß", "ß"}, + {"’", "’"}, + {"‰", "‰"}, + {"€", "€"} + }; + + @Test + public void testProcessingHtml() { + for (String[] pair : TEST_DATA) { + final AtomText atomText = new AtomText("", new Atom(), AtomText.TYPE_HTML); + atomText.setContent(pair[0]); + assertEquals(pair[1], atomText.getProcessedContent()); + } + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/AtomParserTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/AtomParserTest.java new file mode 100644 index 000000000..ba8aaf4f0 --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/AtomParserTest.java @@ -0,0 +1,98 @@ +package de.danoeh.antennapod.parser.feed.element.namespace; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.util.Date; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for Atom feeds in FeedHandler. + */ +@RunWith(RobolectricTestRunner.class) +public class AtomParserTest { + + @Test + public void testAtomBasic() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testAtomBasic.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals(Feed.TYPE_ATOM1, feed.getType()); + assertEquals("title", feed.getTitle()); + 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.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++) { + FeedItem item = feed.getItems().get(i); + assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); + assertEquals("item-" + i, item.getTitle()); + assertNull(item.getDescription()); + assertEquals("http://example.com/items/" + i, item.getLink()); + assertEquals(new Date(i * 60000), item.getPubDate()); + assertNull(item.getPaymentLink()); + assertEquals("http://example.com/picture", item.getImageLocation()); + // media + assertTrue(item.hasMedia()); + FeedMedia media = item.getMedia(); + //noinspection ConstantConditions + assertEquals("http://example.com/media-" + i, media.getDownload_url()); + assertEquals(1024 * 1024, media.getSize()); + assertEquals("audio/mp3", media.getMime_type()); + // chapters + assertNull(item.getChapters()); + } + } + + @Test + public void testEmptyRelLinks() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testEmptyRelLinks.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals(Feed.TYPE_ATOM1, feed.getType()); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com/feed", feed.getFeedIdentifier()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertNull(feed.getPaymentLinks()); + assertEquals("http://example.com/picture", feed.getImageUrl()); + assertEquals(1, feed.getItems().size()); + + // feed entry + FeedItem item = feed.getItems().get(0); + assertEquals("http://example.com/item-0", item.getItemIdentifier()); + assertEquals("item-0", item.getTitle()); + assertNull(item.getDescription()); + assertEquals("http://example.com/items/0", item.getLink()); + assertEquals(new Date(0), item.getPubDate()); + assertNull(item.getPaymentLink()); + assertEquals("http://example.com/picture", item.getImageLocation()); + // media + assertFalse(item.hasMedia()); + // chapters + assertNull(item.getChapters()); + } + + @Test + public void testLogoWithWhitespace() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-atom-testLogoWithWhitespace.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + 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.getPaymentLinks().get(0).url); + assertEquals("https://example.com/image.png", feed.getImageUrl()); + assertEquals(0, feed.getItems().size()); + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/FeedParserTestHelper.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/FeedParserTestHelper.java new file mode 100644 index 000000000..5cc52d8cb --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/FeedParserTestHelper.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.parser.feed.element.namespace; + +import androidx.annotation.NonNull; + +import java.io.File; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.parser.feed.FeedHandler; + +/** + * Tests for FeedHandler. + */ +public abstract class FeedParserTestHelper { + + /** + * Returns the File object for a file in the resources folder. + */ + @NonNull + static File getFeedFile(@NonNull String fileName) { + //noinspection ConstantConditions + return new File(FeedParserTestHelper.class.getClassLoader().getResource(fileName).getFile()); + } + + /** + * Runs the feed parser on the given file. + */ + @NonNull + static Feed runFeedParser(@NonNull File feedFile) throws Exception { + FeedHandler handler = new FeedHandler(); + Feed parsedFeed = new Feed("http://example.com/feed", null); + parsedFeed.setFile_url(feedFile.getAbsolutePath()); + parsedFeed.setDownloaded(true); + handler.parseFeed(parsedFeed); + return parsedFeed; + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java new file mode 100644 index 000000000..8f8942d7b --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/namespace/RssParserTest.java @@ -0,0 +1,99 @@ +package de.danoeh.antennapod.parser.feed.element.namespace; + +import android.text.TextUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.io.File; +import java.util.Date; + +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.playback.MediaType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for RSS feeds in FeedHandler. + */ +@RunWith(RobolectricTestRunner.class) +public class RssParserTest { + + @Test + public void testRss2Basic() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testRss2Basic.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals(Feed.TYPE_RSS2, feed.getType()); + assertEquals("title", feed.getTitle()); + assertEquals("en", feed.getLanguage()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + 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++) { + FeedItem item = feed.getItems().get(i); + assertEquals("http://example.com/item-" + i, item.getItemIdentifier()); + assertEquals("item-" + i, item.getTitle()); + assertNull(item.getDescription()); + assertEquals("http://example.com/items/" + i, item.getLink()); + assertEquals(new Date(i * 60000), item.getPubDate()); + assertNull(item.getPaymentLink()); + assertEquals("http://example.com/picture", item.getImageLocation()); + // media + assertTrue(item.hasMedia()); + FeedMedia media = item.getMedia(); + //noinspection ConstantConditions + assertEquals("http://example.com/media-" + i, media.getDownload_url()); + assertEquals(1024 * 1024, media.getSize()); + assertEquals("audio/mp3", media.getMime_type()); + // chapters + assertNull(item.getChapters()); + } + } + + @Test + public void testImageWithWhitespace() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testImageWithWhitespace.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url); + assertEquals("https://example.com/image.png", feed.getImageUrl()); + assertEquals(0, feed.getItems().size()); + } + + @Test + public void testMediaContentMime() throws Exception { + File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testMediaContentMime.xml"); + Feed feed = FeedParserTestHelper.runFeedParser(feedFile); + assertEquals("title", feed.getTitle()); + assertEquals("http://example.com", feed.getLink()); + assertEquals("This is the description", feed.getDescription()); + assertEquals("http://example.com/payment", feed.getPaymentLinks().get(0).url); + assertNull(feed.getImageUrl()); + assertEquals(1, feed.getItems().size()); + FeedItem feedItem = feed.getItems().get(0); + //noinspection ConstantConditions + 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); + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DateUtilsTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DateUtilsTest.java new file mode 100644 index 000000000..1f039d703 --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DateUtilsTest.java @@ -0,0 +1,175 @@ +package de.danoeh.antennapod.parser.feed.element.util; + +import de.danoeh.antennapod.parser.feed.util.DateUtils; +import org.junit.Test; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +/** + * Unit test for {@link DateUtils}. + */ +public class DateUtilsTest { + + @Test + public void testParseDateWithMicroseconds() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithCentiseconds() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithDeciseconds() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9"); + assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); + assertEquals(900, actual.getTime() % 1000); + } + + @Test + public void testParseDateWithMicrosecondsAndTimezone() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 963); + Date actual = DateUtils.parse("2015-03-28T13:31:04.963870 +0700"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithCentisecondsAndTimezone() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 960); + Date actual = DateUtils.parse("2015-03-28T13:31:04.96 +0700"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithDecisecondsAndTimezone() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 900); + Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700"); + assertEquals(expected.getTime() / 1000, actual.getTime() / 1000); + assertEquals(900, actual.getTime() % 1000); + } + + @Test + public void testParseDateWithTimezoneName() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Sat, 28 Mar 2015 01:31:04 EST"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithTimezoneName2() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 0); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Sat, 28 Mar 2015 01:31 EST"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithTimeZoneOffset() { + GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 12, 16, 12); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Sat, 28 March 2015 08:16:12 -0400"); + assertEquals(expected, actual); + } + + @Test + public void testAsctime() { + GregorianCalendar exp = new GregorianCalendar(2011, 4, 25, 12, 33, 0); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Wed, 25 May 2011 12:33:00"); + assertEquals(expected, actual); + } + + @Test + public void testMultipleConsecutiveSpaces() { + GregorianCalendar exp = new GregorianCalendar(2010, 2, 23, 6, 6, 26); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis()); + Date actual = DateUtils.parse("Tue, 23 Mar 2010 01:06:26 -0500"); + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithNoTimezonePadding() { + GregorianCalendar exp = new GregorianCalendar(2017, 1, 22, 22, 28, 0); + exp.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected = new Date(exp.getTimeInMillis() + 2); + Date actual = DateUtils.parse("2017-02-22T14:28:00.002-08:00"); + assertEquals(expected, actual); + } + + /** + * Requires Android platform. Root cause: {@link DateUtils} implementation makes + * use of ISO 8601 time zone, which does not work on standard JDK. + * + * @see #testParseDateWithNoTimezonePadding() + */ + @Test + public void testParseDateWithForCest() { + GregorianCalendar exp1 = new GregorianCalendar(2017, 0, 28, 22, 0, 0); + exp1.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected1 = new Date(exp1.getTimeInMillis()); + Date actual1 = DateUtils.parse("Sun, 29 Jan 2017 00:00:00 CEST"); + assertEquals(expected1, actual1); + + GregorianCalendar exp2 = new GregorianCalendar(2017, 0, 28, 23, 0, 0); + exp2.setTimeZone(TimeZone.getTimeZone("UTC")); + Date expected2 = new Date(exp2.getTimeInMillis()); + Date actual2 = DateUtils.parse("Sun, 29 Jan 2017 00:00:00 CET"); + assertEquals(expected2, actual2); + } + + @Test + public void testParseDateWithIncorrectWeekday() { + GregorianCalendar exp1 = new GregorianCalendar(2014, 9, 8, 9, 0, 0); + exp1.setTimeZone(TimeZone.getTimeZone("GMT")); + Date expected = new Date(exp1.getTimeInMillis()); + Date actual = DateUtils.parse("Thu, 8 Oct 2014 09:00:00 GMT"); // actually a Wednesday + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithBadAbbreviation() { + GregorianCalendar exp1 = new GregorianCalendar(2014, 8, 8, 0, 0, 0); + exp1.setTimeZone(TimeZone.getTimeZone("GMT")); + Date expected = new Date(exp1.getTimeInMillis()); + Date actual = DateUtils.parse("Mon, 8 Sept 2014 00:00:00 GMT"); // should be Sep + assertEquals(expected, actual); + } + + @Test + public void testParseDateWithTwoTimezones() { + final GregorianCalendar exp1 = new GregorianCalendar(2015, Calendar.MARCH, 1, 1, 0, 0); + exp1.setTimeZone(TimeZone.getTimeZone("GMT-4")); + final Date expected = new Date(exp1.getTimeInMillis()); + final Date actual = DateUtils.parse("Sun 01 Mar 2015 01:00:00 GMT-0400 (EDT)"); + assertEquals(expected, actual); + } +} diff --git a/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DurationParserTest.java b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DurationParserTest.java new file mode 100644 index 000000000..91d9ea5ed --- /dev/null +++ b/parser/feed/src/test/java/de/danoeh/antennapod/parser/feed/element/util/DurationParserTest.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.parser.feed.element.util; + +import de.danoeh.antennapod.parser.feed.util.DurationParser; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class DurationParserTest { + private int milliseconds = 1; + private int seconds = 1000 * milliseconds; + private int minutes = 60 * seconds; + private int hours = 60 * minutes; + + @Test + public void testSecondDurationInMillis() { + long duration = DurationParser.inMillis("00:45"); + assertEquals(45 * seconds, duration); + } + + @Test + public void testSingleNumberDurationInMillis() { + int twoHoursInSeconds = 2 * 60 * 60; + long duration = DurationParser.inMillis(String.valueOf(twoHoursInSeconds)); + assertEquals(2 * hours, duration); + } + + @Test + public void testMinuteSecondDurationInMillis() { + long duration = DurationParser.inMillis("05:10"); + assertEquals(5 * minutes + 10 * seconds, duration); + } + + @Test + public void testHourMinuteSecondDurationInMillis() { + long duration = DurationParser.inMillis("02:15:45"); + assertEquals(2 * hours + 15 * minutes + 45 * seconds, duration); + } + + @Test + public void testSecondsWithMillisecondsInMillis() { + long duration = DurationParser.inMillis("00:00:00.123"); + assertEquals(123, duration); + } +} diff --git a/parser/feed/src/test/resources/feed-atom-testAtomBasic.xml b/parser/feed/src/test/resources/feed-atom-testAtomBasic.xml new file mode 100644 index 000000000..cefc4f979 --- /dev/null +++ b/parser/feed/src/test/resources/feed-atom-testAtomBasic.xml @@ -0,0 +1 @@ +http://example.com/feedtitleThis is the descriptionhttp://example.com/picturehttp://example.com/item-0item-01970-01-01T00:00:00Zhttp://example.com/item-1item-11970-01-01T00:01:00Zhttp://example.com/item-2item-21970-01-01T00:02:00Zhttp://example.com/item-3item-31970-01-01T00:03:00Zhttp://example.com/item-4item-41970-01-01T00:04:00Zhttp://example.com/item-5item-51970-01-01T00:05:00Zhttp://example.com/item-6item-61970-01-01T00:06:00Zhttp://example.com/item-7item-71970-01-01T00:07:00Zhttp://example.com/item-8item-81970-01-01T00:08:00Zhttp://example.com/item-9item-91970-01-01T00:09:00Z \ No newline at end of file diff --git a/parser/feed/src/test/resources/feed-atom-testEmptyRelLinks.xml b/parser/feed/src/test/resources/feed-atom-testEmptyRelLinks.xml new file mode 100644 index 000000000..04c28ef67 --- /dev/null +++ b/parser/feed/src/test/resources/feed-atom-testEmptyRelLinks.xml @@ -0,0 +1,14 @@ + + + http://example.com/feed + title + + This is the description + http://example.com/picture + + http://example.com/item-0 + item-0 + + 1970-01-01T00:00:00Z + + diff --git a/parser/feed/src/test/resources/feed-atom-testLogoWithWhitespace.xml b/parser/feed/src/test/resources/feed-atom-testLogoWithWhitespace.xml new file mode 100644 index 000000000..f4886d56a --- /dev/null +++ b/parser/feed/src/test/resources/feed-atom-testLogoWithWhitespace.xml @@ -0,0 +1,2 @@ +http://example.com/feedtitleThis is the description https://example.com/image.png + \ No newline at end of file diff --git a/parser/feed/src/test/resources/feed-rss-testImageWithWhitespace.xml b/parser/feed/src/test/resources/feed-rss-testImageWithWhitespace.xml new file mode 100644 index 000000000..2be9401d2 --- /dev/null +++ b/parser/feed/src/test/resources/feed-rss-testImageWithWhitespace.xml @@ -0,0 +1,2 @@ +titleThis is the descriptionhttp://example.comen https://example.com/image.png + \ No newline at end of file diff --git a/parser/feed/src/test/resources/feed-rss-testMediaContentMime.xml b/parser/feed/src/test/resources/feed-rss-testMediaContentMime.xml new file mode 100644 index 000000000..a715abb37 --- /dev/null +++ b/parser/feed/src/test/resources/feed-rss-testMediaContentMime.xml @@ -0,0 +1 @@ +titleThis is the descriptionhttp://example.comen \ No newline at end of file diff --git a/parser/feed/src/test/resources/feed-rss-testMultipleFundingTags.xml b/parser/feed/src/test/resources/feed-rss-testMultipleFundingTags.xml new file mode 100644 index 000000000..2535bda32 --- /dev/null +++ b/parser/feed/src/test/resources/feed-rss-testMultipleFundingTags.xml @@ -0,0 +1,9 @@ + + + + title + + Text 1 + Text 2 + + diff --git a/parser/feed/src/test/resources/feed-rss-testRss2Basic.xml b/parser/feed/src/test/resources/feed-rss-testRss2Basic.xml new file mode 100644 index 000000000..dd771b61a --- /dev/null +++ b/parser/feed/src/test/resources/feed-rss-testRss2Basic.xml @@ -0,0 +1 @@ +titleThis is the descriptionhttp://example.comenhttp://example.com/pictureitem-0http://example.com/items/001 Jan 70 01:00:00 +0100http://example.com/item-0item-1http://example.com/items/101 Jan 70 01:01:00 +0100http://example.com/item-1item-2http://example.com/items/201 Jan 70 01:02:00 +0100http://example.com/item-2item-3http://example.com/items/301 Jan 70 01:03:00 +0100http://example.com/item-3item-4http://example.com/items/401 Jan 70 01:04:00 +0100http://example.com/item-4item-5http://example.com/items/501 Jan 70 01:05:00 +0100http://example.com/item-5item-6http://example.com/items/601 Jan 70 01:06:00 +0100http://example.com/item-6item-7http://example.com/items/701 Jan 70 01:07:00 +0100http://example.com/item-7item-8http://example.com/items/801 Jan 70 01:08:00 +0100http://example.com/item-8item-9http://example.com/items/901 Jan 70 01:09:00 +0100http://example.com/item-9 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 27cc1a863..edf55858e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,8 @@ include ':net:ssl' include ':net:sync:gpoddernet' include ':net:sync:model' +include ':parser:feed' + include ':ui:app-start-intent' include ':ui:common' include ':ui:png-icons' -- cgit v1.2.3