summaryrefslogtreecommitdiff
path: root/ui/chapters/src
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2024-04-05 19:20:27 +0200
committerGitHub <noreply@github.com>2024-04-05 19:20:27 +0200
commit92ab575b150ab49ca85e0ac994558142e49c9e68 (patch)
tree422dcd76895a4ba06ed02723ff61351435c0fe49 /ui/chapters/src
parent2143ab135182434911d4554a8ef08115eaa0d2d0 (diff)
downloadAntennaPod-92ab575b150ab49ca85e0ac994558142e49c9e68.zip
Delete core module (#7060)
Diffstat (limited to 'ui/chapters/src')
-rw-r--r--ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java70
-rw-r--r--ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java228
2 files changed, 298 insertions, 0 deletions
diff --git a/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java
new file mode 100644
index 000000000..7ec11b566
--- /dev/null
+++ b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java
@@ -0,0 +1,70 @@
+package de.danoeh.antennapod.ui.chapters;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.model.feed.Chapter;
+
+import java.util.List;
+
+public class ChapterMerger {
+ private static final String TAG = "ChapterMerger";
+
+ private ChapterMerger() {
+
+ }
+
+ /**
+ * This method might modify the input data.
+ */
+ @Nullable
+ public static List<Chapter> merge(@Nullable List<Chapter> chapters1, @Nullable List<Chapter> chapters2) {
+ Log.d(TAG, "Merging chapters");
+ if (chapters1 == null) {
+ return chapters2;
+ } else if (chapters2 == null) {
+ return chapters1;
+ } else if (chapters2.size() > chapters1.size()) {
+ return chapters2;
+ } else if (chapters2.size() < chapters1.size()) {
+ return chapters1;
+ } else {
+ // Merge chapter lists of same length. Store in chapters2 array.
+ // In case the lists can not be merged, return chapters1 array.
+ for (int i = 0; i < chapters2.size(); i++) {
+ Chapter chapterTarget = chapters2.get(i);
+ Chapter chapterOther = chapters1.get(i);
+
+ if (Math.abs(chapterTarget.getStart() - chapterOther.getStart()) > 1000) {
+ Log.e(TAG, "Chapter lists are too different. Cancelling merge.");
+ return score(chapters1) > score(chapters2) ? chapters1 : chapters2;
+ }
+
+ if (TextUtils.isEmpty(chapterTarget.getImageUrl())) {
+ chapterTarget.setImageUrl(chapterOther.getImageUrl());
+ }
+ if (TextUtils.isEmpty(chapterTarget.getLink())) {
+ chapterTarget.setLink(chapterOther.getLink());
+ }
+ if (TextUtils.isEmpty(chapterTarget.getTitle())) {
+ chapterTarget.setTitle(chapterOther.getTitle());
+ }
+ }
+ return chapters2;
+ }
+ }
+
+ /**
+ * Tries to give a score that can determine which list of chapters a user might want to see.
+ */
+ private static int score(List<Chapter> chapters) {
+ int score = 0;
+ for (Chapter chapter : chapters) {
+ score = score
+ + (TextUtils.isEmpty(chapter.getTitle()) ? 0 : 1)
+ + (TextUtils.isEmpty(chapter.getLink()) ? 0 : 1)
+ + (TextUtils.isEmpty(chapter.getImageUrl()) ? 0 : 1);
+ }
+ return score;
+ }
+}
diff --git a/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java
new file mode 100644
index 000000000..5554890ed
--- /dev/null
+++ b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java
@@ -0,0 +1,228 @@
+package de.danoeh.antennapod.ui.chapters;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.net.common.AntennapodHttpClient;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser;
+import de.danoeh.antennapod.parser.media.id3.ChapterReader;
+import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
+import okhttp3.CacheControl;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.io.input.CountingInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Utility class for getting chapter data from media files.
+ */
+public class ChapterUtils {
+
+ private static final String TAG = "ChapterUtils";
+
+ private ChapterUtils() {
+ }
+
+ public static void loadChapters(Playable playable, Context context, boolean forceRefresh) {
+ if (playable.getChapters() != null && !forceRefresh) {
+ // Already loaded
+ return;
+ }
+
+ try {
+ List<Chapter> chaptersFromDatabase = null;
+ List<Chapter> chaptersFromPodcastIndex = null;
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ if (feedMedia.getItem() == null) {
+ feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
+ }
+ if (feedMedia.getItem().hasChapters()) {
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
+ }
+
+ if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) {
+ chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl(
+ feedMedia.getItem().getPodcastIndexChapterUrl(), forceRefresh);
+ }
+
+ }
+
+ List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
+ List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex);
+ if (chapters == null) {
+ // Do not try loading again. There are no chapters or parsing failed.
+ playable.setChapters(Collections.emptyList());
+ } else {
+ playable.setChapters(chapters);
+ }
+ } catch (InterruptedIOException e) {
+ Log.d(TAG, "Chapter loading interrupted");
+ playable.setChapters(null); // Allow later retry
+ }
+ }
+
+ public static List<Chapter> loadChaptersFromMediaFile(Playable playable, Context context)
+ throws InterruptedIOException {
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readId3ChaptersFrom(in);
+ if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
+ return chapters;
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException | ID3ReaderException e) {
+ Log.e(TAG, "Unable to load ID3 chapters: " + e.getMessage());
+ }
+
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readOggChaptersFromInputStream(in);
+ if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
+ return chapters;
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException | VorbisCommentReaderException e) {
+ Log.e(TAG, "Unable to load vorbis chapters: " + e.getMessage());
+ }
+ return null;
+ }
+
+ private static CountingInputStream openStream(Playable playable, Context context) throws IOException {
+ if (playable.localFileAvailable()) {
+ if (playable.getLocalFileUrl() == null) {
+ throw new IOException("No local url");
+ }
+ File source = new File(playable.getLocalFileUrl());
+ if (!source.exists()) {
+ throw new IOException("Local file does not exist");
+ }
+ return new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
+ } else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
+ Uri uri = Uri.parse(playable.getStreamUrl());
+ return new CountingInputStream(new BufferedInputStream(context.getContentResolver().openInputStream(uri)));
+ } else {
+ Request request = new Request.Builder().url(playable.getStreamUrl()).build();
+ Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.body() == null) {
+ throw new IOException("Body is null");
+ }
+ return new CountingInputStream(new BufferedInputStream(response.body().byteStream()));
+ }
+ }
+
+ public static List<Chapter> loadChaptersFromUrl(String url, boolean forceRefresh) throws InterruptedIOException {
+ if (forceRefresh) {
+ return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK);
+ }
+ List<Chapter> cachedChapters = loadChaptersFromUrl(url, CacheControl.FORCE_CACHE);
+ if (cachedChapters == null || cachedChapters.size() <= 1) {
+ // Some publishers use one dummy chapter before actual chapters are available
+ return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK);
+ }
+ return cachedChapters;
+ }
+
+ private static List<Chapter> loadChaptersFromUrl(String url, CacheControl cacheControl)
+ throws InterruptedIOException {
+ Response response = null;
+ try {
+ Request request = new Request.Builder().url(url).cacheControl(cacheControl).build();
+ response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.isSuccessful() && response.body() != null) {
+ return PodcastIndexChapterParser.parse(response.body().string());
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (response != null) {
+ response.close();
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
+ ChapterReader reader = new ChapterReader(in);
+ reader.readInputStream();
+ List<Chapter> chapters = reader.getChapters();
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (!chaptersValid(chapters)) {
+ Log.e(TAG, "Chapter data was invalid");
+ return Collections.emptyList();
+ }
+ return chapters;
+ }
+
+ @NonNull
+ private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader(new BufferedInputStream(input));
+ reader.readInputStream();
+ List<Chapter> chapters = reader.getChapters();
+ if (chapters == null) {
+ return Collections.emptyList();
+ }
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (chaptersValid(chapters)) {
+ return chapters;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Makes sure that chapter does a title and an item attribute.
+ */
+ private static void enumerateEmptyChapterTitles(List<Chapter> chapters) {
+ for (int i = 0; i < chapters.size(); i++) {
+ Chapter c = chapters.get(i);
+ if (c.getTitle() == null) {
+ c.setTitle(Integer.toString(i));
+ }
+ }
+ }
+
+ private static boolean chaptersValid(List<Chapter> chapters) {
+ if (chapters.isEmpty()) {
+ return false;
+ }
+ for (Chapter c : chapters) {
+ if (c.getStart() < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static class ChapterStartTimeComparator implements Comparator<Chapter> {
+ @Override
+ public int compare(Chapter lhs, Chapter rhs) {
+ return Long.compare(lhs.getStart(), rhs.getStart());
+ }
+ }
+}