Merge pull request #4590 from ByteHamster/merge-chapters

Merge chapter lists if specified in both feed and media file
This commit is contained in:
ByteHamster 2020-10-25 19:20:05 +01:00 committed by GitHub
commit 71b6c57773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 48 deletions

View File

@ -375,7 +375,7 @@ public class DBReaderTest {
for (Feed feed : feeds) { for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) { for (FeedItem item : feed.getItems()) {
assertFalse(item.hasChapters()); assertFalse(item.hasChapters());
DBReader.loadChaptersOfFeedItem(item); item.setChapters(DBReader.loadChaptersOfFeedItem(item));
assertFalse(item.hasChapters()); assertFalse(item.hasChapters());
assertNull(item.getChapters()); assertNull(item.getChapters());
} }
@ -390,7 +390,7 @@ public class DBReaderTest {
for (Feed feed : feeds) { for (Feed feed : feeds) {
for (FeedItem item : feed.getItems()) { for (FeedItem item : feed.getItems()) {
assertTrue(item.hasChapters()); assertTrue(item.hasChapters());
DBReader.loadChaptersOfFeedItem(item); item.setChapters(DBReader.loadChaptersOfFeedItem(item));
assertTrue(item.hasChapters()); assertTrue(item.hasChapters());
assertNotNull(item.getChapters()); assertNotNull(item.getChapters());
assertEquals(NUM_CHAPTERS, item.getChapters().size()); assertEquals(NUM_CHAPTERS, item.getChapters().size());
@ -404,7 +404,7 @@ public class DBReaderTest {
List<Feed> feeds = saveFeedlist(1, 1, false, true, NUM_CHAPTERS); List<Feed> feeds = saveFeedlist(1, 1, false, true, NUM_CHAPTERS);
FeedItem item1 = feeds.get(0).getItems().get(0); FeedItem item1 = feeds.get(0).getItems().get(0);
FeedItem item2 = DBReader.getFeedItem(item1.getId()); FeedItem item2 = DBReader.getFeedItem(item1.getId());
DBReader.loadChaptersOfFeedItem(item2); item2.setChapters(DBReader.loadChaptersOfFeedItem(item2));
assertTrue(item2.hasChapters()); assertTrue(item2.hasChapters());
assertEquals(item1.getChapters(), item2.getChapters()); assertEquals(item1.getChapters(), item2.getChapters());
} }

View File

@ -0,0 +1,55 @@
package de.danoeh.antennapod.core.feed;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
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.start - chapterOther.start) > 1000) {
Log.e(TAG, "Chapter lists are too different. Cancelling merge.");
return chapters1;
}
if (TextUtils.isEmpty(chapterTarget.imageUrl)) {
chapterTarget.imageUrl = chapterOther.imageUrl;
}
if (TextUtils.isEmpty(chapterTarget.link)) {
chapterTarget.link = chapterOther.link;
}
if (TextUtils.isEmpty(chapterTarget.title)) {
chapterTarget.title = chapterOther.title;
}
}
return chapters2;
}
}
}

View File

@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -382,19 +383,30 @@ public class FeedMedia extends FeedFile implements Playable {
if (item == null || item.getChapters() != null) { if (item == null || item.getChapters() != null) {
return; return;
} }
// check if chapters are stored in db and not loaded yet.
List<Chapter> chapters = loadChapters();
if (chapters == null) {
// Do not try loading again. There are no chapters.
item.setChapters(Collections.emptyList());
} else {
item.setChapters(chapters);
}
}
private List<Chapter> loadChapters() {
List<Chapter> chaptersFromDatabase = null;
if (item.hasChapters()) { if (item.hasChapters()) {
DBReader.loadChaptersOfFeedItem(item); chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item);
} else { }
List<Chapter> chaptersFromMediaFile;
if (localFileAvailable()) { if (localFileAvailable()) {
ChapterUtils.loadChaptersFromFileUrl(this); chaptersFromMediaFile = ChapterUtils.loadChaptersFromFileUrl(this);
} else { } else {
ChapterUtils.loadChaptersFromStreamUrl(this); chaptersFromMediaFile = ChapterUtils.loadChaptersFromStreamUrl(this);
}
if (item.getChapters() != null) {
DBWriter.setFeedItem(item);
}
} }
return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
} }
@Override @Override

View File

@ -56,7 +56,7 @@ public class MediaDownloadedHandler implements Runnable {
// check if file has chapters // check if file has chapters
if (media.getItem() != null && !media.getItem().hasChapters()) { if (media.getItem() != null && !media.getItem().hasChapters()) {
ChapterUtils.loadChaptersFromFileUrl(media); media.setChapters(ChapterUtils.loadChaptersFromFileUrl(media));
} }
// Get duration // Get duration

View File

@ -651,29 +651,30 @@ public final class DBReader {
* *
* @param item The FeedItem * @param item The FeedItem
*/ */
public static void loadChaptersOfFeedItem(final FeedItem item) { public static List<Chapter> loadChaptersOfFeedItem(final FeedItem item) {
Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]"); Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]");
PodDBAdapter adapter = PodDBAdapter.getInstance(); PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open(); adapter.open();
try { try {
loadChaptersOfFeedItem(adapter, item); return loadChaptersOfFeedItem(adapter, item);
} finally { } finally {
adapter.close(); adapter.close();
} }
} }
private static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { private static List<Chapter> loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) { try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) {
int chaptersCount = cursor.getCount(); int chaptersCount = cursor.getCount();
if (chaptersCount == 0) { if (chaptersCount == 0) {
item.setChapters(null); item.setChapters(null);
return; return null;
} }
item.setChapters(new ArrayList<>(chaptersCount)); ArrayList<Chapter> chapters = new ArrayList<>();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
item.getChapters().add(Chapter.fromCursor(cursor)); chapters.add(Chapter.fromCursor(cursor));
} }
return chapters;
} }
} }

View File

@ -52,32 +52,34 @@ public class ChapterUtils {
return chapters.size() - 1; return chapters.size() - 1;
} }
public static void loadChaptersFromStreamUrl(Playable media) { public static List<Chapter> loadChaptersFromStreamUrl(Playable media) {
ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media); List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media);
if (media.getChapters() == null) { if (chapters == null) {
ChapterUtils.readOggChaptersFromPlayableStreamUrl(media); chapters = ChapterUtils.readOggChaptersFromPlayableStreamUrl(media);
} }
return chapters;
} }
public static void loadChaptersFromFileUrl(Playable media) { public static List<Chapter> loadChaptersFromFileUrl(Playable media) {
if (!media.localFileAvailable()) { if (!media.localFileAvailable()) {
Log.e(TAG, "Could not load chapters from file url: local file not available"); Log.e(TAG, "Could not load chapters from file url: local file not available");
return; return null;
} }
ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
if (media.getChapters() == null) { if (chapters == null) {
ChapterUtils.readOggChaptersFromPlayableFileUrl(media); chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
} }
return chapters;
} }
/** /**
* Uses the download URL of a media object of a feeditem to read its ID3 * Uses the download URL of a media object of a feeditem to read its ID3
* chapters. * chapters.
*/ */
private static void readID3ChaptersFromPlayableStreamUrl(Playable p) { private static List<Chapter> readID3ChaptersFromPlayableStreamUrl(Playable p) {
if (p == null || p.getStreamUrl() == null) { if (p == null || p.getStreamUrl() == null) {
Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null"); Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null");
return; return null;
} }
Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
CountingInputStream in = null; CountingInputStream in = null;
@ -88,7 +90,7 @@ public class ChapterUtils {
in = new CountingInputStream(urlConnection.getInputStream()); in = new CountingInputStream(urlConnection.getInputStream());
List<Chapter> chapters = readChaptersFrom(in); List<Chapter> chapters = readChaptersFrom(in);
if (!chapters.isEmpty()) { if (!chapters.isEmpty()) {
p.setChapters(chapters); return chapters;
} }
Log.i(TAG, "Chapters loaded"); Log.i(TAG, "Chapters loaded");
} catch (IOException | ID3ReaderException e) { } catch (IOException | ID3ReaderException e) {
@ -96,21 +98,22 @@ public class ChapterUtils {
} finally { } finally {
IOUtils.closeQuietly(in); IOUtils.closeQuietly(in);
} }
return null;
} }
/** /**
* Uses the file URL of a media object of a feeditem to read its ID3 * Uses the file URL of a media object of a feeditem to read its ID3
* chapters. * chapters.
*/ */
private static void readID3ChaptersFromPlayableFileUrl(Playable p) { private static List<Chapter> readID3ChaptersFromPlayableFileUrl(Playable p) {
if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) { if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) {
return; return null;
} }
Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
File source = new File(p.getLocalMediaUrl()); File source = new File(p.getLocalMediaUrl());
if (!source.exists()) { if (!source.exists()) {
Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist");
return; return null;
} }
CountingInputStream in = null; CountingInputStream in = null;
@ -118,7 +121,7 @@ public class ChapterUtils {
in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source))); in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
List<Chapter> chapters = readChaptersFrom(in); List<Chapter> chapters = readChaptersFrom(in);
if (!chapters.isEmpty()) { if (!chapters.isEmpty()) {
p.setChapters(chapters); return chapters;
} }
Log.i(TAG, "Chapters loaded"); Log.i(TAG, "Chapters loaded");
} catch (IOException | ID3ReaderException e) { } catch (IOException | ID3ReaderException e) {
@ -126,6 +129,7 @@ public class ChapterUtils {
} finally { } finally {
IOUtils.closeQuietly(in); IOUtils.closeQuietly(in);
} }
return null;
} }
@NonNull @NonNull
@ -147,9 +151,9 @@ public class ChapterUtils {
return chapters; return chapters;
} }
private static void readOggChaptersFromPlayableStreamUrl(Playable media) { private static List<Chapter> readOggChaptersFromPlayableStreamUrl(Playable media) {
if (media == null || !media.streamAvailable()) { if (media == null || !media.streamAvailable()) {
return; return null;
} }
InputStream input = null; InputStream input = null;
try { try {
@ -158,34 +162,36 @@ public class ChapterUtils {
urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
input = urlConnection.getInputStream(); input = urlConnection.getInputStream();
if (input != null) { if (input != null) {
readOggChaptersFromInputStream(media, input); return readOggChaptersFromInputStream(media, input);
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e)); Log.e(TAG, Log.getStackTraceString(e));
} finally { } finally {
IOUtils.closeQuietly(input); IOUtils.closeQuietly(input);
} }
return null;
} }
private static void readOggChaptersFromPlayableFileUrl(Playable media) { private static List<Chapter> readOggChaptersFromPlayableFileUrl(Playable media) {
if (media == null || media.getLocalMediaUrl() == null) { if (media == null || media.getLocalMediaUrl() == null) {
return; return null;
} }
File source = new File(media.getLocalMediaUrl()); File source = new File(media.getLocalMediaUrl());
if (source.exists()) { if (source.exists()) {
InputStream input = null; InputStream input = null;
try { try {
input = new BufferedInputStream(new FileInputStream(source)); input = new BufferedInputStream(new FileInputStream(source));
readOggChaptersFromInputStream(media, input); return readOggChaptersFromInputStream(media, input);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.e(TAG, Log.getStackTraceString(e)); Log.e(TAG, Log.getStackTraceString(e));
} finally { } finally {
IOUtils.closeQuietly(input); IOUtils.closeQuietly(input);
} }
} }
return null;
} }
private static void readOggChaptersFromInputStream(Playable p, InputStream input) { private static List<Chapter> readOggChaptersFromInputStream(Playable p, InputStream input) {
Log.d(TAG, "Trying to read chapters from item with title " + p.getEpisodeTitle()); Log.d(TAG, "Trying to read chapters from item with title " + p.getEpisodeTitle());
try { try {
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
@ -193,19 +199,20 @@ public class ChapterUtils {
List<Chapter> chapters = reader.getChapters(); List<Chapter> chapters = reader.getChapters();
if (chapters == null) { if (chapters == null) {
Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters"); Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters");
return; return null;
} }
Collections.sort(chapters, new ChapterStartTimeComparator()); Collections.sort(chapters, new ChapterStartTimeComparator());
enumerateEmptyChapterTitles(chapters); enumerateEmptyChapterTitles(chapters);
if (chaptersValid(chapters)) { if (chaptersValid(chapters)) {
p.setChapters(chapters);
Log.i(TAG, "Chapters loaded"); Log.i(TAG, "Chapters loaded");
return chapters;
} else { } else {
Log.e(TAG, "Chapter data was invalid"); Log.e(TAG, "Chapter data was invalid");
} }
} catch (VorbisCommentReaderException e) { } catch (VorbisCommentReaderException e) {
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
/** /**

View File

@ -99,7 +99,7 @@ public class ExternalMedia implements Playable {
e.printStackTrace(); e.printStackTrace();
throw new PlayableException("NumberFormatException when reading duration of media file"); throw new PlayableException("NumberFormatException when reading duration of media file");
} }
ChapterUtils.loadChaptersFromFileUrl(this); setChapters(ChapterUtils.loadChaptersFromFileUrl(this));
} }
@Override @Override

View File

@ -130,7 +130,7 @@ public class RemoteMedia implements Playable {
@Override @Override
public void loadChapterMarks() { public void loadChapterMarks() {
ChapterUtils.loadChaptersFromStreamUrl(this); setChapters(ChapterUtils.loadChaptersFromStreamUrl(this));
} }
@Override @Override