Merge pull request #4590 from ByteHamster/merge-chapters
Merge chapter lists if specified in both feed and media file
This commit is contained in:
commit
71b6c57773
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue