Support for podcast 2.0 chapters (#5630)

This commit is contained in:
Tony Tam 2022-03-06 07:09:09 -08:00 committed by GitHub
parent dad4e405d4
commit 1a1bf02e8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 8 deletions

View File

@ -60,6 +60,9 @@ public class MediaDownloadedHandler implements Runnable {
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context)); media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context));
} }
if (media.getItem() != null && media.getItem().getPodcastIndexChapterUrl() != null) {
ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl());
}
// Get duration // Get duration
MediaMetadataRetriever mmr = new MediaMetadataRetriever(); MediaMetadataRetriever mmr = new MediaMetadataRetriever();
String durationStr = null; String durationStr = null;

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Chapter;
@ -11,11 +12,13 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser;
import de.danoeh.antennapod.parser.media.id3.ChapterReader; import de.danoeh.antennapod.parser.media.id3.ChapterReader;
import de.danoeh.antennapod.parser.media.id3.ID3ReaderException; import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader; import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException; import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
import okhttp3.CacheControl;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.input.CountingInputStream;
@ -57,6 +60,7 @@ public class ChapterUtils {
} }
List<Chapter> chaptersFromDatabase = null; List<Chapter> chaptersFromDatabase = null;
List<Chapter> chaptersFromPodcastIndex = null;
if (playable instanceof FeedMedia) { if (playable instanceof FeedMedia) {
FeedMedia feedMedia = (FeedMedia) playable; FeedMedia feedMedia = (FeedMedia) playable;
if (feedMedia.getItem() == null) { if (feedMedia.getItem() == null) {
@ -65,10 +69,17 @@ public class ChapterUtils {
if (feedMedia.getItem().hasChapters()) { if (feedMedia.getItem().hasChapters()) {
chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem()); chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
} }
if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) {
chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl(
feedMedia.getItem().getPodcastIndexChapterUrl());
}
} }
List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context); List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex);
if (chapters == null) { if (chapters == null) {
// Do not try loading again. There are no chapters. // Do not try loading again. There are no chapters.
playable.setChapters(Collections.emptyList()); playable.setChapters(Collections.emptyList());
@ -123,6 +134,27 @@ public class ChapterUtils {
} }
} }
public static List<Chapter> loadChaptersFromUrl(String url) {
try {
Request request = new Request.Builder().url(url).cacheControl(CacheControl.FORCE_CACHE).build();
Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
if (response.isSuccessful() && response.body() != null) {
List<Chapter> chapters = PodcastIndexChapterParser.parse(response.body().string());
if (chapters != null && !chapters.isEmpty()) {
return chapters;
}
}
request = new Request.Builder().url(url).build();
response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
if (response.isSuccessful() && response.body() != null) {
return PodcastIndexChapterParser.parse(response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@NonNull @NonNull
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException { private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
ChapterReader reader = new ChapterReader(in); ChapterReader reader = new ChapterReader(in);

View File

@ -41,6 +41,7 @@ public class FeedItem extends FeedComponent implements Serializable {
private transient Feed feed; private transient Feed feed;
private long feedId; private long feedId;
private String podcastIndexChapterUrl;
private int state; private int state;
public static final int NEW = -1; public static final int NEW = -1;
@ -81,7 +82,7 @@ public class FeedItem extends FeedComponent implements Serializable {
* */ * */
public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId, public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
boolean hasChapters, String imageUrl, int state, boolean hasChapters, String imageUrl, int state,
String itemIdentifier, long autoDownload) { String itemIdentifier, long autoDownload, String podcastIndexChapterUrl) {
this.id = id; this.id = id;
this.title = title; this.title = title;
this.link = link; this.link = link;
@ -93,6 +94,7 @@ public class FeedItem extends FeedComponent implements Serializable {
this.state = state; this.state = state;
this.itemIdentifier = itemIdentifier; this.itemIdentifier = itemIdentifier;
this.autoDownload = autoDownload; this.autoDownload = autoDownload;
this.podcastIndexChapterUrl = podcastIndexChapterUrl;
} }
/** /**
@ -157,6 +159,9 @@ public class FeedItem extends FeedComponent implements Serializable {
chapters = other.chapters; chapters = other.chapters;
} }
} }
if (other.podcastIndexChapterUrl != null) {
podcastIndexChapterUrl = other.podcastIndexChapterUrl;
}
} }
/** /**
@ -427,6 +432,14 @@ public class FeedItem extends FeedComponent implements Serializable {
tags.remove(tag); tags.remove(tag);
} }
public String getPodcastIndexChapterUrl() {
return podcastIndexChapterUrl;
}
public void setPodcastIndexChapterUrl(String url) {
podcastIndexChapterUrl = url;
}
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {

View File

@ -0,0 +1,31 @@
package de.danoeh.antennapod.parser.feed;
import de.danoeh.antennapod.model.feed.Chapter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public class PodcastIndexChapterParser {
public static List<Chapter> parse(String jsonStr) {
try {
List<Chapter> chapters = new ArrayList<>();
JSONObject obj = new JSONObject(jsonStr);
JSONArray objChapters = obj.getJSONArray("chapters");
for (int i = 0; i < objChapters.length(); i++) {
JSONObject jsonObject = objChapters.getJSONObject(i);
int startTime = jsonObject.optInt("startTime", 0);
String title = jsonObject.optString("title");
String link = jsonObject.optString("url");
String img = jsonObject.optString("img");
chapters.add(new Chapter(startTime * 1000L, title, link, img));
}
return chapters;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -1,8 +1,8 @@
package de.danoeh.antennapod.parser.feed.namespace; package de.danoeh.antennapod.parser.feed.namespace;
import android.text.TextUtils;
import de.danoeh.antennapod.parser.feed.HandlerState; import de.danoeh.antennapod.parser.feed.HandlerState;
import de.danoeh.antennapod.parser.feed.element.SyndElement; import de.danoeh.antennapod.parser.feed.element.SyndElement;
import org.jsoup.helper.StringUtil;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import de.danoeh.antennapod.model.feed.FeedFunding; import de.danoeh.antennapod.model.feed.FeedFunding;
@ -13,6 +13,7 @@ public class PodcastIndex extends Namespace {
public static final String NSURI2 = "https://podcastindex.org/namespace/1.0"; public static final String NSURI2 = "https://podcastindex.org/namespace/1.0";
private static final String URL = "url"; private static final String URL = "url";
private static final String FUNDING = "funding"; private static final String FUNDING = "funding";
private static final String CHAPTERS = "chapters";
@Override @Override
public SyndElement handleElementStart(String localName, HandlerState state, public SyndElement handleElementStart(String localName, HandlerState state,
@ -22,6 +23,11 @@ public class PodcastIndex extends Namespace {
FeedFunding funding = new FeedFunding(href, ""); FeedFunding funding = new FeedFunding(href, "");
state.setCurrentFunding(funding); state.setCurrentFunding(funding);
state.getFeed().addPayment(state.getCurrentFunding()); state.getFeed().addPayment(state.getCurrentFunding());
} else if (CHAPTERS.equals(localName)) {
String href = attributes.getValue(URL);
if (!TextUtils.isEmpty(href)) {
state.getCurrentItem().setPodcastIndexChapterUrl(href);
}
} }
return new SyndElement(localName, this); return new SyndElement(localName, this);
} }
@ -32,7 +38,7 @@ public class PodcastIndex extends Namespace {
return; return;
} }
String content = state.getContentBuf().toString(); String content = state.getContentBuf().toString();
if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !StringUtil.isBlank(content)) { if (FUNDING.equals(localName) && state.getCurrentFunding() != null && !TextUtils.isEmpty(content)) {
state.getCurrentFunding().setContent(content); state.getCurrentFunding().setContent(content);
} }
} }

View File

@ -326,6 +326,10 @@ class DBUpgrader {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1"); + " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1");
} }
if (oldVersion < 2060000) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ " ADD COLUMN " + PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL + " TEXT");
}
} }
} }

View File

@ -50,7 +50,7 @@ public class PodDBAdapter {
private static final String TAG = "PodDBAdapter"; private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db"; public static final String DATABASE_NAME = "Antennapod.db";
public static final int VERSION = 2050000; public static final int VERSION = 2060000;
/** /**
* Maximum number of arguments for IN-operator. * Maximum number of arguments for IN-operator.
@ -116,6 +116,7 @@ public class PodDBAdapter {
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
public static final String KEY_FEED_TAGS = "tags"; public static final String KEY_FEED_TAGS = "tags";
public static final String KEY_EPISODE_NOTIFICATION = "episode_notification"; public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
public static final String KEY_PODCASTINDEX_CHAPTER_URL = "podcastindex_chapter_url";
// Table names // Table names
public static final String TABLE_NAME_FEEDS = "Feeds"; public static final String TABLE_NAME_FEEDS = "Feeds";
@ -166,7 +167,8 @@ public class PodDBAdapter {
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
+ KEY_IMAGE_URL + " TEXT," + KEY_IMAGE_URL + " TEXT,"
+ KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)"; + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER,"
+ KEY_PODCASTINDEX_CHAPTER_URL + " TEXT)";
private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
@ -292,7 +294,8 @@ public class PodDBAdapter {
+ TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS; + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS + ", "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PODCASTINDEX_CHAPTER_URL;
private static final String KEYS_FEED_MEDIA = private static final String KEYS_FEED_MEDIA =
TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", " TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", "
@ -648,6 +651,7 @@ public class PodDBAdapter {
values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier());
values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime()); values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime());
values.put(KEY_IMAGE_URL, item.getImageUrl()); values.put(KEY_IMAGE_URL, item.getImageUrl());
values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl());
if (item.getId() == 0) { if (item.getId() == 0) {
item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));

View File

@ -27,6 +27,7 @@ public abstract class FeedItemCursorMapper {
int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER); int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER);
int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS); int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS);
int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL); int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
int indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL);
long id = cursor.getInt(indexId); long id = cursor.getInt(indexId);
String title = cursor.getString(indexTitle); String title = cursor.getString(indexTitle);
@ -39,8 +40,9 @@ public abstract class FeedItemCursorMapper {
String itemIdentifier = cursor.getString(indexItemIdentifier); String itemIdentifier = cursor.getString(indexItemIdentifier);
long autoDownload = cursor.getLong(indexAutoDownload); long autoDownload = cursor.getLong(indexAutoDownload);
String imageUrl = cursor.getString(indexImageUrl); String imageUrl = cursor.getString(indexImageUrl);
String podcastIndexChapterUrl = cursor.getString(indexPodcastIndexChapterUrl);
return new FeedItem(id, title, link, pubDate, paymentLink, feedId, return new FeedItem(id, title, link, pubDate, paymentLink, feedId,
hasChapters, imageUrl, state, itemIdentifier, autoDownload); hasChapters, imageUrl, state, itemIdentifier, autoDownload, podcastIndexChapterUrl);
} }
} }